feat: implement invoice payments component and integrate with supplier invoices

This commit is contained in:
2025-12-29 10:14:28 +03:30
parent 2583bc7ad5
commit 85a9c8714d
24 changed files with 268 additions and 47 deletions
@@ -63,8 +63,6 @@ export class CardexFiltersComponent {
this.store.setDefaultInventory(inventory);
}
setDefaultProduct(product: IProductResponse) {
console.log('setDefaultProduct');
this.store.setDefaultProduct(product);
}
}
+27 -14
View File
@@ -17,6 +17,7 @@ export interface CardexState extends PaginatedState<ICardexResponse> {
selectedProduct: Maybe<ICardexProductSummary>;
variant: 'inventory' | 'product' | 'general';
canChangeVariant: boolean;
cardexTitle: string;
}
@Injectable({
@@ -63,6 +64,7 @@ export class CardexStore extends PaginatedStore<ICardexResponse, CardexState> {
selectedProduct: null,
variant: 'general',
canChangeVariant: true,
cardexTitle: 'کاردکس',
});
this.initial();
}
@@ -70,18 +72,7 @@ export class CardexStore extends PaginatedStore<ICardexResponse, CardexState> {
readonly filters = computed(() => this._state().filters);
readonly variant = computed(() => this._state().variant);
readonly canChangeVariant = computed(() => this._state().canChangeVariant);
readonly cardexTitle = computed(() => {
switch (this._state().variant) {
case 'product':
return `کاردکس کالای ${this._state().selectedProduct?.name}`;
case 'inventory':
return `کاردکس انبار ${this._state().selectedInventory?.name}`;
default:
return `کاردکس ${this._state().selectedProduct ? 'کالای ' + this._state().selectedProduct?.name : ''} ${
this._state().selectedInventory ? 'در انبار ' + this._state().selectedInventory?.name : ''
}`;
}
});
readonly cardexTitle = computed(() => this._state().cardexTitle);
/**
* Reset state to initial values
@@ -109,6 +100,7 @@ export class CardexStore extends PaginatedStore<ICardexResponse, CardexState> {
selectedProduct: null,
variant: 'general',
canChangeVariant: this.state().canChangeVariant,
cardexTitle: 'کاردکس',
});
}
@@ -155,8 +147,6 @@ export class CardexStore extends PaginatedStore<ICardexResponse, CardexState> {
updateFilter(filters: ICardexRequestPayload) {
this.updateVariant();
console.log('filters');
console.log(filters);
const cleanedFilters = this.validateFilters() || {};
this.router
@@ -169,6 +159,7 @@ export class CardexStore extends PaginatedStore<ICardexResponse, CardexState> {
console.log(err);
});
this.patchState({ filters });
this.updateCardexTitle();
}
setProduct(product: Maybe<ICardexProductSummary>) {
@@ -184,10 +175,12 @@ export class CardexStore extends PaginatedStore<ICardexResponse, CardexState> {
setDefaultInventory(inventory: ICardexInventorySummary) {
this.patchState({ selectedInventory: inventory });
this.updateCardexTitle();
}
setDefaultProduct(product: ICardexProductSummary) {
this.patchState({ selectedProduct: product });
this.updateCardexTitle();
}
private updateVariant() {
@@ -201,7 +194,27 @@ export class CardexStore extends PaginatedStore<ICardexResponse, CardexState> {
} else if (productIsSelected) {
variant = 'product';
}
this.updateCardexTitle();
this.patchState({ variant });
}
updateCardexTitle() {
let title = '';
switch (this._state().variant) {
case 'product':
title = `کاردکس کالای ${this._state().selectedProduct?.name}`;
break;
case 'inventory':
title = `کاردکس انبار ${this._state().selectedInventory?.name}`;
break;
default:
title = `کاردکس ${this._state().selectedProduct ? 'کالای ' + this._state().selectedProduct?.name : ''} ${
this._state().selectedInventory ? 'در انبار ' + this._state().selectedInventory?.name : ''
}`;
break;
}
this.patchState({ cardexTitle: title });
}
}
@@ -29,6 +29,10 @@ export class InventoryBankAccountSelectComponent extends AbstractSelectComponent
constructor(private service: InventoryBankAccountsService) {
super();
}
ngOnInit() {
console.log(this.inventoryId);
this.getData();
}
@@ -1,15 +1,7 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
export type TInventoriesRouteNames =
| 'inventories'
| 'inventory'
| 'transfer'
| 'products'
| 'productCardex'
| 'posAccounts';
export const inventoriesNamedRoutes: NamedRoutes<TInventoriesRouteNames> = {
export const inventoriesNamedRoutes: NamedRoutes<any> = {
inventories: {
path: 'inventories',
loadComponent: () => import('../../views/list.component').then((m) => m.InventoriesComponent),
@@ -65,6 +57,17 @@ export const inventoriesNamedRoutes: NamedRoutes<TInventoriesRouteNames> = {
pagePath: () => '/inventories/:inventoryId/pos-accounts',
},
},
};
purchase: {
path: 'inventories/:inventoryId/purchase',
loadComponent: () =>
import('../../views/purchase.component').then((m) => m.InventoryPurchaseComponent),
meta: {
title: 'ثبت خرید در انبار',
pagePath: () => '/inventories/:inventoryId/purchase',
},
},
} as const;
export type TInventoriesRouteNames = keyof typeof inventoriesNamedRoutes;
export const INVENTORIES_ROUTES: Routes = Object.values(inventoriesNamedRoutes);
@@ -21,3 +21,24 @@ export interface IInventoryBankAccountRequest extends IBankAccountsRequest {}
export interface IInventoryAssignBankAccountRequest {
bankAccountId: number;
}
export interface IBankAccountSummary {
branch: Branch;
accountNumber: string;
cardNumber: string;
name: string;
id: number;
iban: string;
}
interface Branch {
id: number;
name: string;
bank: Bank;
}
interface Bank {
id: number;
name: string;
shortName: string;
}
@@ -43,10 +43,13 @@ export class InventoryStore extends EntityStore<IInventoryDetailResponse, Invent
this.patchState({ loading: true });
this.service.getSingle(inventoryId).subscribe({
next: (res) => {
console.log('first');
this.patchState({
entities: { [res.id]: res },
loading: false,
});
console.log(this.entities());
},
error: (err) => {
this.patchState({ loading: false, error: err });
@@ -0,0 +1,4 @@
@if (loading()) {
} @else {
<purchase-receipt-template [inventory]="inventory()" />
}
@@ -0,0 +1,22 @@
import { PurchaseReceiptTemplateComponent } from '@/modules/purchases/components';
import { Component, computed, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { InventoryStore } from '../store/inventory.store';
@Component({
selector: 'app-inventory-purchase',
templateUrl: './purchase.component.html',
imports: [PurchaseReceiptTemplateComponent],
})
export class InventoryPurchaseComponent {
private readonly route = inject(ActivatedRoute);
private readonly store = inject(InventoryStore);
inventoryId = this.route.snapshot.paramMap.get('inventoryId')!;
inventory = computed(() => this.store.entities()[this.inventoryId]);
loading = this.store.loading;
constructor() {
this.store.getSingle(this.inventoryId);
}
}
@@ -14,14 +14,19 @@
icon="pi pi-plus"
[routerLink]="['/inventories', inventoryId, 'pos-accounts']"
></button>
<button pButton type="button" label="افزایش موجودی" icon="pi pi-plus"></button>
<!-- [routerLink]="['/inventories', inventoryId, 'purchase']" -->
<button
pButton
type="button"
label="افزایش موجودی"
icon="pi pi-plus"
[routerLink]="['/inventories', inventoryId, 'purchase']"
></button>
<button
pButton
type="button"
label="انتقال کالا"
icon="pi pi-plus"
[routerLink]="['/inventories', inventoryId, 'transfer']"
[routerLink]="['/inventories', 'transfer']"
></button>
<button pButton type="button" label="ویرایش" icon="pi pi-pencil"></button>
<!-- [routerLink]="['/inventories', inventoryId, 'update']" -->
@@ -151,8 +151,8 @@
<span class="font-bold" [appPriceMask]="totalAmount"></span>
</div>
<div class="flex justify-between">
<span>مالیات (۹٪):</span>
<span class="font-bold" [appPriceMask]="totalAmount * 0.09"></span>
<span>مالیات (۱۰٪):</span>
<span class="font-bold" [appPriceMask]="totalAmount * 0.1"></span>
</div>
</div>
</td>
@@ -161,12 +161,12 @@
<td [attr.colspan]="6" class="p-0! border-none!">
<div class="w-full flex items-center gap-2 py-10 border-surface-700 border-t-4">
<span>مبلغ قابل پرداخت:</span>
<span [appPriceAlphabet]="totalAmount + totalAmount * 0.09" class="font-bold text-lg"></span>
<span [appPriceAlphabet]="totalAmount + totalAmount * 0.1" class="font-bold text-lg"></span>
</div>
</td>
<td [attr.colspan]="2" class="p-0! border-none!">
<div class="flex h-full items-center justify-end py-10 px-2 border-surface-700 border-t-4 border-r">
<span [appPriceMask]="totalAmount + totalAmount * 0.09" class="text-xl font-bold"></span>
<span [appPriceMask]="totalAmount + totalAmount * 0.1" class="text-xl font-bold"></span>
</div>
</td>
</tr>
@@ -10,7 +10,7 @@ import { IColumn } from '@/shared/components/pageDataList/page-data-list.compone
import { PriceAlphabetDirective, PriceMaskDirective } from '@/shared/directives';
import { UikitFlatpickrJalaliComponent } from '@/uikit';
import { CommonModule } from '@angular/common';
import { Component, inject, Input, signal } from '@angular/core';
import { Component, effect, inject, Input, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import dayjs from 'dayjs';
import { ButtonDirective } from 'primeng/button';
@@ -130,6 +130,15 @@ export class PurchaseReceiptTemplateComponent {
this.form.controls.paidAmount.setValue(0);
}
});
effect(() => {
if (this.inventory) {
this.form.controls.inventory.setValue(this.inventory);
}
if (this.supplier) {
this.form.controls.supplier.setValue(this.supplier);
}
});
}
formIsSubmitted = signal(false);
@@ -0,0 +1,5 @@
<app-page-data-list [items]="items()" [columns]="columns" [loading]="loading()" pageTitle="پرداخت‌های فاکتور">
<ng-template #paymentMethodTpl let-item>
<app-catalog-payment-method-type-tag [type]="item.paymentMethod" />
</ng-template>
</app-page-data-list>
@@ -0,0 +1,75 @@
import { CatalogPaymentMethodTypeTagComponent } from '@/shared/catalog/paymentMethodTypes';
import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { Component, Input, signal, TemplateRef, ViewChild } from '@angular/core';
import { IInvoicePaymentResponse } from '../../models/invoices.io';
import { SupplierInvoicesService } from '../../services';
@Component({
selector: 'app-invoice-payments',
templateUrl: './payments.component.html',
imports: [PageDataListComponent, CatalogPaymentMethodTypeTagComponent],
})
export class InvoicePaymentsComponent {
@Input() supplierId!: string;
@Input() invoiceId!: string;
@ViewChild('paymentMethodTpl', { static: true }) paymentMethodTpl!: TemplateRef<any>;
constructor(private readonly service: SupplierInvoicesService) {}
columns = [] as IColumn[];
ngOnInit() {
console.log(this.invoiceId);
this.columns = [
{
field: 'index',
header: 'ردیف',
type: 'index',
},
{
field: 'code',
header: 'شناسه پرداخت',
},
{
field: 'amount',
header: 'مبلغ پرداختی',
type: 'price',
},
{
field: 'paymentMethod',
header: 'روش پرداخت',
customDataModel: this.paymentMethodTpl,
},
{
field: 'bankAccount',
header: 'حساب بانکی',
customDataModel: (item: IInvoicePaymentResponse) =>
`${item.bankAccount.name} - بانک ${item.bankAccount.branch.bank.name} - شعبه ${item.bankAccount.branch.name}`,
},
{
field: 'payedAt',
header: 'تاریخ پرداخت',
type: 'date',
},
];
this.getData();
}
items = signal<IInvoicePaymentResponse[]>([]);
loading = signal(false);
getData() {
this.loading.set(true);
return this.service.getPayments(this.supplierId!, this.invoiceId!).subscribe({
next: (res) => {
this.items.set(res.data);
this.loading.set(false);
},
error: (err) => {
this.loading.set(false);
},
});
}
}
@@ -4,7 +4,7 @@
<span class="text-xl font-bold">دفتر کل تامین‌کننده</span>
<div class="flex items-center gap-2">
@if (showViewAllCTA) {
<button pButton outlined [routerLink]="['/suppliers', supplierId, 'ledger']">مشاهده‌ی تمامی تراکنش‌ها</button>
<button pButton outlined [routerLink]="['/suppliers', supplierId, 'ledger']">مشاهده‌ی دفتر کل</button>
}
<button pButton outlined icon="pi pi-refresh" [loading]="loading()" (click)="getAll()"></button>
</div>
@@ -33,7 +33,7 @@
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="7" class="text-center! p-10!">هیچ فاکتوری ثبت نشده است.</td>
<td colspan="7" class="text-center! p-10!">هیچ رکوردی ثبت نشده است.</td>
</tr>
</ng-template>
</p-table>
@@ -4,5 +4,7 @@ export const SUPPLIER_INVOICES_API_ROUTES = (_baseUrl: string) => {
list: (supplierId: string) => `${baseUrl(supplierId)}`,
single: (supplierId: string, invoiceId: string) => `${baseUrl(supplierId)}/${invoiceId}`,
pay: (supplierId: string, invoiceId: string) => `${baseUrl(supplierId)}/${invoiceId}/pay`,
invoicePayments: (supplierId: string, invoiceId: string) =>
`${baseUrl(supplierId)}/${invoiceId}/payments`,
};
};
@@ -1,8 +1,9 @@
import ISummary from '@/core/models/summary';
import { IBankAccountSummary } from '@/modules/inventories/models';
import { PurchaseReceiptStatus } from '@/shared/catalog';
import { PaymentMethodType } from '@/shared/catalog/paymentMethodTypes';
import { PaymentType } from '@/shared/catalog/paymentTypes';
import { ISupplierReceiptItemSummary } from './types';
import { IReceiptInfo, ISupplierReceiptItemSummary } from './types';
export interface ISupplierInvoicesRawResponse {
id: number;
@@ -37,3 +38,18 @@ export interface IPayRawResponse {
description?: string;
}
export interface IPayResponse extends IPayRawResponse {}
export interface IInvoicePaymentRawResponse {
id: number;
amount: string;
paymentMethod: string;
type: string;
bankAccountId: number;
payedAt: string;
description: null;
createdAt: string;
receipt: IReceiptInfo;
bankAccount: IBankAccountSummary;
}
export interface IInvoicePaymentResponse extends IInvoicePaymentRawResponse {}
+16
View File
@@ -1,3 +1,5 @@
import ISummary from '@/core/models/summary';
export interface IReceiptsOverview {
totalPayment: string;
count: number;
@@ -19,3 +21,17 @@ export interface ISupplierReceiptItemSummary {
sku: string;
};
}
export interface IReceiptInfo {
id: number;
code: string;
totalAmount: string;
paidAmount: string;
description: string;
createdAt: string;
updatedAt: string;
status: string;
supplierId: number;
inventoryId: number;
inventory: ISummary;
}
@@ -4,6 +4,8 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { SUPPLIERS_API_ROUTES } from '../constants';
import {
IInvoicePaymentRawResponse,
IInvoicePaymentResponse,
IPayRawResponse,
IPayRequestPayload,
IPayResponse,
@@ -36,4 +38,13 @@ export class SupplierInvoicesService {
): Observable<IPayResponse> {
return this.http.post<IPayRawResponse>(this.apiRoutes.pay(supplierId, invoiceId), payload);
}
getPayments(
supplierId: string,
invoiceId: string,
): Observable<IPaginatedResponse<IInvoicePaymentResponse>> {
return this.http.get<IPaginatedResponse<IInvoicePaymentRawResponse>>(
this.apiRoutes.invoicePayments(supplierId, invoiceId),
);
}
}
@@ -21,6 +21,7 @@
</app-key-value>
</div>
</div>
<app-invoice-payments [supplierId]="supplierId" [invoiceId]="invoiceId" />
@if (invoice() && supplier()) {
<app-supplier-invoice-pay-form
[(visible)]="showPayForm"
@@ -5,6 +5,7 @@ import { Component, computed, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ButtonDirective } from 'primeng/button';
import { SupplierInvoicePayFormComponent } from '../components/invoices/pay-form.component';
import { InvoicePaymentsComponent } from '../components/invoices/payments.component';
import { SupplierInvoiceStore, SupplierStore } from '../store';
@Component({
@@ -16,6 +17,7 @@ import { SupplierInvoiceStore, SupplierStore } from '../store';
CatalogPurchaseReceiptStatusTagComponent,
ButtonDirective,
SupplierInvoicePayFormComponent,
InvoicePaymentsComponent,
],
})
export class SupplierInvoiceComponent {
@@ -27,6 +29,7 @@ export class SupplierInvoiceComponent {
this.supplierStore.getSingle(this.supplierId);
this.store.supplierId = this.supplierId;
this.store.getSingle(this.invoiceId);
console.log(this.supplierId);
}
supplierId = this.route.snapshot.params['supplierId'];
@@ -1,14 +1,16 @@
<div class="flex flex-col gap-4">
<app-inner-pages-header [pageTitle]="pageTitle()" [backRoute]="['/suppliers', supplierId]" />
<div class="inline-flex items-center gap-5">
<div class="">
<app-key-value label="شماره تماس" [value]="supplier()?.mobileNumber" />
</div>
@if (supplier()) {
<div class="">
<app-key-value label="شماره تماس" [value]="supplier().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() || ''" />
<details-info-card [icon]="item.icon" [label]="item.label" [value]="item.value.toString() || ''" />
}
</div>
</div>
@@ -1,5 +1,4 @@
import { Maybe } from '@/core';
import { PriceMaskDirective } from '@/shared/directives';
import { UikitFieldComponent } from '@/uikit/uikit-field.component';
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
@@ -20,7 +19,6 @@ import { ToggleSwitch } from 'primeng/toggleswitch';
InputGroupModule,
UikitFieldComponent,
ToggleSwitch,
PriceMaskDirective,
InputGroupAddon,
InputNumberModule,
],
@@ -41,7 +41,15 @@
<ng-template #header let-columns>
<tr>
@for (col of columns; track col.header) {
<th [style]="{ width: col.width, minWidth: col.minWidth }">{{ col.header }}</th>
<th
[style]="
col.type === 'index'
? { width: '3rem', minWidth: '3rem' }
: { width: col.width, minWidth: col.minWidth }
"
>
{{ col.header }}
</th>
}
@if (actionsCount()) {
<th type="th" [style]="{ width: `${actionsCount() * 2 + (actionsCount() - 1) * 0.25}rem` }"></th>
@@ -49,11 +57,13 @@
</tr>
</ng-template>
<ng-template #body let-item>
<ng-template #body let-item let-i="rowIndex">
<tr>
@for (col of columns; track col.field) {
<td>
@if (getTemplate(col)) {
@if (col.type === "index") {
{{ i * (currentPage || 1) + 1 }}
} @else if (getTemplate(col)) {
<ng-container
[ngTemplateOutlet]="getTemplate(col)"
[ngTemplateOutletContext]="{ $implicit: item }"
@@ -24,7 +24,7 @@ export interface IColumn<T = any> {
width?: string;
minWidth?: string;
canCopy?: boolean;
type?: 'text' | 'price' | 'boolean' | 'date' | 'nested';
type?: 'text' | 'price' | 'boolean' | 'date' | 'nested' | 'index';
nestedPath?: string;
customDataModel?: TemplateRef<any> | ((item: T) => string | number | boolean);
}