From 879e29f54fea5f0f58624dd994e166fb0d4b3e5c Mon Sep 17 00:00:00 2001 From: ahasani194 Date: Sat, 27 Dec 2025 20:35:02 +0330 Subject: [PATCH] feat: enhance input component to support price type and add new payment method components - Updated InputComponent to handle 'price' type with appropriate formatting and validation. - Introduced new API routes for purchase receipt payments. - Created constants for purchase receipts and invoices. - Developed SupplierInvoicePayFormComponent for processing invoice payments. - Implemented SupplierLedgerComponent to display supplier transactions. - Added services and stores for managing supplier invoices and payments. - Created components for selecting payment methods and displaying payment type tags. - Enhanced UI with new templates for invoices and payment forms. - Added utility functions for handling payment method types and statuses. --- src/app.routes.ts | 2 - src/app/core/state/base-store.ts | 4 +- .../components/form.component.html | 4 +- .../constants/apiRoutes/index.ts | 5 + .../constants/index.ts | 0 .../constants/routes/index.ts | 9 ++ .../stores/components/form.component.html | 8 -- .../stores/components/form.component.ts | 68 -------------- .../stores/constants/apiRoutes/index.ts | 6 -- .../modules/stores/constants/routes/index.ts | 25 ----- src/app/modules/stores/models/index.ts | 1 - src/app/modules/stores/models/io.d.ts | 16 ---- .../modules/stores/services/main.service.ts | 25 ----- src/app/modules/stores/views/index.ts | 2 - .../modules/stores/views/list.component.html | 14 --- .../modules/stores/views/list.component.ts | 49 ---------- .../stores/views/single.component.html | 1 - .../modules/stores/views/single.component.ts | 9 -- .../invoices/invoices.component.html | 23 ++++- .../components/invoices/invoices.component.ts | 13 ++- .../invoices/pay-form.component.html | 23 +++++ .../components/invoices/pay-form.component.ts | 57 ++++++++++++ .../components/ledger/ledger.component.html | 41 ++++++++ .../components/ledger/ledger.component.ts | 56 +++++++++++ .../suppliers/constants/apiRoutes/index.ts | 7 +- .../suppliers/constants/apiRoutes/invoices.ts | 8 ++ .../suppliers/constants/routes/index.ts | 26 +++++- .../modules/suppliers/models/invoices.io.ts | 39 ++++++++ src/app/modules/suppliers/models/io.d.ts | 16 +--- src/app/modules/suppliers/services/index.ts | 2 + .../suppliers/services/invoices.service.ts | 39 ++++++++ .../suppliers/services/main.service.ts | 15 +-- src/app/modules/suppliers/store/index.ts | 2 + .../modules/suppliers/store/invoice.store.ts | 78 ++++++++++++++++ .../modules/suppliers/store/invoices.store.ts | 93 +++++++++++++++++++ src/app/modules/suppliers/store/main.store.ts | 72 ++++++++++++++ .../suppliers/views/invoice.component.html | 33 +++++++ .../suppliers/views/invoice.component.ts | 58 ++++++++++++ .../suppliers/views/invoices.component.html | 16 ++++ .../suppliers/views/invoices.component.ts | 50 ++++++++++ .../suppliers/views/list.component.html | 1 + .../modules/suppliers/views/list.component.ts | 10 +- .../suppliers/views/single.component.html | 55 +---------- .../suppliers/views/single.component.ts | 6 +- .../shared/catalog/banks/store/main.store.ts | 4 +- src/app/shared/catalog/index.ts | 1 + .../paymentMethodTypes/components/index.ts | 2 + .../components/select/select.component.html | 13 +++ .../components/select/select.component.ts | 28 ++++++ .../components/tag/tag.component.html | 3 + .../components/tag/tag.component.ts | 21 +++++ .../catalog/paymentMethodTypes/index.ts | 3 + .../catalog/paymentMethodTypes/types.ts | 9 ++ .../catalog/paymentMethodTypes/utils.ts | 32 +++++++ .../catalog/paymentTypes/components/index.ts | 1 + .../components/tag/tag.component.html | 3 + .../components/tag/tag.component.ts | 21 +++++ src/app/shared/catalog/paymentTypes/index.ts | 3 + src/app/shared/catalog/paymentTypes/types.ts | 6 ++ src/app/shared/catalog/paymentTypes/utils.ts | 20 ++++ .../catalog/purchaseReceiptStatus/index.ts | 3 + .../purchaseReceiptStatus/tag.component.html | 3 + .../purchaseReceiptStatus/tag.component.ts | 21 +++++ .../catalog/purchaseReceiptStatus/types.ts | 8 ++ .../catalog/purchaseReceiptStatus/utils.ts | 24 +++++ .../abstract-form-dialog.component.ts | 23 +++-- .../inner-pages-header.component.html | 15 +++ .../inner-pages-header.component.ts | 17 ++++ .../components/input/input.component.html | 21 +++++ .../components/input/input.component.ts | 36 ++++++- 70 files changed, 1094 insertions(+), 334 deletions(-) create mode 100644 src/app/modules/purchaseReceipts/constants/apiRoutes/index.ts rename src/app/modules/{stores => purchaseReceipts}/constants/index.ts (100%) create mode 100644 src/app/modules/purchaseReceipts/constants/routes/index.ts delete mode 100644 src/app/modules/stores/components/form.component.html delete mode 100644 src/app/modules/stores/components/form.component.ts delete mode 100644 src/app/modules/stores/constants/apiRoutes/index.ts delete mode 100644 src/app/modules/stores/constants/routes/index.ts delete mode 100644 src/app/modules/stores/models/index.ts delete mode 100644 src/app/modules/stores/models/io.d.ts delete mode 100644 src/app/modules/stores/services/main.service.ts delete mode 100644 src/app/modules/stores/views/index.ts delete mode 100644 src/app/modules/stores/views/list.component.html delete mode 100644 src/app/modules/stores/views/list.component.ts delete mode 100644 src/app/modules/stores/views/single.component.html delete mode 100644 src/app/modules/stores/views/single.component.ts create mode 100644 src/app/modules/suppliers/components/invoices/pay-form.component.html create mode 100644 src/app/modules/suppliers/components/invoices/pay-form.component.ts create mode 100644 src/app/modules/suppliers/components/ledger/ledger.component.html create mode 100644 src/app/modules/suppliers/components/ledger/ledger.component.ts create mode 100644 src/app/modules/suppliers/constants/apiRoutes/invoices.ts create mode 100644 src/app/modules/suppliers/models/invoices.io.ts create mode 100644 src/app/modules/suppliers/services/index.ts create mode 100644 src/app/modules/suppliers/services/invoices.service.ts create mode 100644 src/app/modules/suppliers/store/index.ts create mode 100644 src/app/modules/suppliers/store/invoice.store.ts create mode 100644 src/app/modules/suppliers/store/invoices.store.ts create mode 100644 src/app/modules/suppliers/store/main.store.ts create mode 100644 src/app/modules/suppliers/views/invoice.component.html create mode 100644 src/app/modules/suppliers/views/invoice.component.ts create mode 100644 src/app/modules/suppliers/views/invoices.component.html create mode 100644 src/app/modules/suppliers/views/invoices.component.ts create mode 100644 src/app/shared/catalog/paymentMethodTypes/components/index.ts create mode 100644 src/app/shared/catalog/paymentMethodTypes/components/select/select.component.html create mode 100644 src/app/shared/catalog/paymentMethodTypes/components/select/select.component.ts create mode 100644 src/app/shared/catalog/paymentMethodTypes/components/tag/tag.component.html create mode 100644 src/app/shared/catalog/paymentMethodTypes/components/tag/tag.component.ts create mode 100644 src/app/shared/catalog/paymentMethodTypes/index.ts create mode 100644 src/app/shared/catalog/paymentMethodTypes/types.ts create mode 100644 src/app/shared/catalog/paymentMethodTypes/utils.ts create mode 100644 src/app/shared/catalog/paymentTypes/components/index.ts create mode 100644 src/app/shared/catalog/paymentTypes/components/tag/tag.component.html create mode 100644 src/app/shared/catalog/paymentTypes/components/tag/tag.component.ts create mode 100644 src/app/shared/catalog/paymentTypes/index.ts create mode 100644 src/app/shared/catalog/paymentTypes/types.ts create mode 100644 src/app/shared/catalog/paymentTypes/utils.ts create mode 100644 src/app/shared/catalog/purchaseReceiptStatus/index.ts create mode 100644 src/app/shared/catalog/purchaseReceiptStatus/tag.component.html create mode 100644 src/app/shared/catalog/purchaseReceiptStatus/tag.component.ts create mode 100644 src/app/shared/catalog/purchaseReceiptStatus/types.ts create mode 100644 src/app/shared/catalog/purchaseReceiptStatus/utils.ts create mode 100644 src/app/shared/components/innerPagesHeader/inner-pages-header.component.html create mode 100644 src/app/shared/components/innerPagesHeader/inner-pages-header.component.ts diff --git a/src/app.routes.ts b/src/app.routes.ts index 794e96c..78b19cb 100644 --- a/src/app.routes.ts +++ b/src/app.routes.ts @@ -8,7 +8,6 @@ import { POSComponent } from '@/modules/pos/views/pos.component'; import { PRODUCT_BRANDS_ROUTES } from '@/modules/productBrands/constants'; import { PRODUCT_CATEGORIES_ROUTES } from '@/modules/productCategories/constants'; import { PRODUCTS_ROUTES } from '@/modules/products/constants'; -import { STORES_ROUTES } from '@/modules/stores/constants'; import { SUPPLIERS_ROUTES } from '@/modules/suppliers/constants'; import { USERS_ROUTES } from '@/modules/users/constants'; import { Routes } from '@angular/router'; @@ -24,7 +23,6 @@ export const appRoutes: Routes = [ children: [ { path: '', component: Dashboard }, ...USERS_ROUTES, - ...STORES_ROUTES, ...SUPPLIERS_ROUTES, ...PRODUCT_BRANDS_ROUTES, ...PRODUCT_CATEGORIES_ROUTES, diff --git a/src/app/core/state/base-store.ts b/src/app/core/state/base-store.ts index e625df1..5f0e1f2 100644 --- a/src/app/core/state/base-store.ts +++ b/src/app/core/state/base-store.ts @@ -9,9 +9,6 @@ import { IPaginatedQuery, IResponseMetadata } from '../models/service.model'; export interface BaseState { loading: boolean; error: Maybe; - currentPage?: number; - items?: T; - meta?: Maybe; isRefreshing?: boolean; initialized: boolean; } @@ -21,6 +18,7 @@ export interface BaseState { */ export interface PaginatedState extends BaseState { items: T[]; + meta?: Maybe; totalCount: number; currentPage: number; pageSize: number; diff --git a/src/app/modules/bankBranches/components/form.component.html b/src/app/modules/bankBranches/components/form.component.html index fbe06a1..d691b3a 100644 --- a/src/app/modules/bankBranches/components/form.component.html +++ b/src/app/modules/bankBranches/components/form.component.html @@ -7,8 +7,8 @@ [closable]="true" >
- - + + diff --git a/src/app/modules/purchaseReceipts/constants/apiRoutes/index.ts b/src/app/modules/purchaseReceipts/constants/apiRoutes/index.ts new file mode 100644 index 0000000..35f43f0 --- /dev/null +++ b/src/app/modules/purchaseReceipts/constants/apiRoutes/index.ts @@ -0,0 +1,5 @@ +const baseUrl = '/api/v1/purchase-receipt-payments'; + +export const PURCHASE_RECEIPT_PAYMENTS_API_ROUTES = { + list: () => `${baseUrl}`, +}; diff --git a/src/app/modules/stores/constants/index.ts b/src/app/modules/purchaseReceipts/constants/index.ts similarity index 100% rename from src/app/modules/stores/constants/index.ts rename to src/app/modules/purchaseReceipts/constants/index.ts diff --git a/src/app/modules/purchaseReceipts/constants/routes/index.ts b/src/app/modules/purchaseReceipts/constants/routes/index.ts new file mode 100644 index 0000000..80ad55d --- /dev/null +++ b/src/app/modules/purchaseReceipts/constants/routes/index.ts @@ -0,0 +1,9 @@ +import { NamedRoutes } from '@/core'; +import { Routes } from '@angular/router'; + +export const purchaseReceiptsNamedRoutes: NamedRoutes = {} as const; + +export type TPurchaseReceiptsRouteNames = + (typeof purchaseReceiptsNamedRoutes)[keyof typeof purchaseReceiptsNamedRoutes]; + +export const PURCHASE_RECEIPTS_ROUTES: Routes = Object.values(purchaseReceiptsNamedRoutes); diff --git a/src/app/modules/stores/components/form.component.html b/src/app/modules/stores/components/form.component.html deleted file mode 100644 index 4c89f5c..0000000 --- a/src/app/modules/stores/components/form.component.html +++ /dev/null @@ -1,8 +0,0 @@ - -
- - - - - -
diff --git a/src/app/modules/stores/components/form.component.ts b/src/app/modules/stores/components/form.component.ts deleted file mode 100644 index de58e79..0000000 --- a/src/app/modules/stores/components/form.component.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { InputComponent } from '@/shared/components'; -import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component'; -import { Component, effect, EventEmitter, inject, Input, Output, signal } from '@angular/core'; -import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; -import { Dialog } from 'primeng/dialog'; -import { IStoreRequest, IStoreResponse } from '../models'; -import { StoresService } from '../services/main.service'; - -@Component({ - selector: 'store-form', - templateUrl: './form.component.html', - imports: [ReactiveFormsModule, Dialog, InputComponent, FormFooterActionsComponent], -}) -export class StoreFormComponent { - @Input() initialValues?: IStoreResponse; - - @Input() - set visible(v: boolean) { - this.visibleSignal.set(!!v); - } - get visible() { - return this.visibleSignal(); - } - private visibleSignal = signal(false); - - @Output() visibleChange = new EventEmitter(); - @Output() onSubmit = new EventEmitter(); - - private fb = inject(FormBuilder); - constructor(private service: StoresService) { - effect(() => { - const v = this.visibleSignal(); - if (!v) this.form.reset(); - }); - } - - form = this.fb.group({ - name: [this.initialValues?.name || null, [Validators.required]], - location: [this.initialValues?.location || null, [Validators.required]], - isActive: [this.initialValues?.isActive || false, [Validators.required]], - }); - - submitLoading = signal(false); - - submit() { - this.form.markAllAsTouched(); - if (this.form.valid) { - this.form.disable(); - this.submitLoading.set(true); - this.service.create(this.form.value as IStoreRequest).subscribe({ - next: (res) => { - this.submitLoading.set(false); - this.form.enable(); - this.form.reset(); - this.onSubmit.emit(res); - }, - error: () => { - this.submitLoading.set(false); - this.form.enable(); - }, - }); - } - } - - close() { - this.visibleChange.emit(false); - } -} diff --git a/src/app/modules/stores/constants/apiRoutes/index.ts b/src/app/modules/stores/constants/apiRoutes/index.ts deleted file mode 100644 index 3fc2cdc..0000000 --- a/src/app/modules/stores/constants/apiRoutes/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -const baseUrl = '/api/v1/stores'; - -export const STORES_API_ROUTES = { - list: () => `${baseUrl}`, - single: (storeId: string) => `${baseUrl}/${storeId}`, -}; diff --git a/src/app/modules/stores/constants/routes/index.ts b/src/app/modules/stores/constants/routes/index.ts deleted file mode 100644 index 3840c58..0000000 --- a/src/app/modules/stores/constants/routes/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NamedRoutes } from '@/core'; -import { Routes } from '@angular/router'; - -export type TStoresRouteNames = 'stores' | 'store'; - -export const storesNamedRoutes: NamedRoutes = { - stores: { - path: 'stores', - loadComponent: () => import('../../views/list.component').then((m) => m.StoresComponent), - meta: { - title: 'فروشگاه‌ها', - pagePath: () => '/stores', - }, - }, - store: { - path: 'stores/:storeId', - loadComponent: () => import('../../views/single.component').then((m) => m.StoreComponent), - meta: { - title: 'فروشگاه', - pagePath: () => '/stores/:storeId', - }, - }, -}; - -export const STORES_ROUTES: Routes = Object.values(storesNamedRoutes); diff --git a/src/app/modules/stores/models/index.ts b/src/app/modules/stores/models/index.ts deleted file mode 100644 index 61a518a..0000000 --- a/src/app/modules/stores/models/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './io'; diff --git a/src/app/modules/stores/models/io.d.ts b/src/app/modules/stores/models/io.d.ts deleted file mode 100644 index 6978821..0000000 --- a/src/app/modules/stores/models/io.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface IStoreRawResponse { - id: number; - name: string; - location: string; - isActive: boolean; - createdAt: Date; - updatedAt: Date; -} - -export interface IStoreResponse extends IStoreRawResponse {} - -export interface IStoreRequest { - name: string; - location: string; - isActive: boolean; -} diff --git a/src/app/modules/stores/services/main.service.ts b/src/app/modules/stores/services/main.service.ts deleted file mode 100644 index 619d9f5..0000000 --- a/src/app/modules/stores/services/main.service.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { IPaginatedResponse } from '@/core/models/service.model'; -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; -import { STORES_API_ROUTES } from '../constants'; -import { IStoreRawResponse, IStoreRequest, IStoreResponse } from '../models'; - -@Injectable({ providedIn: 'root' }) -export class StoresService { - constructor(private http: HttpClient) {} - - private apiRoutes = STORES_API_ROUTES; - - getAll(): Observable> { - return this.http.get>(this.apiRoutes.list()); - } - - getSingle(storeId: string): Observable { - return this.http.get(this.apiRoutes.single(storeId)); - } - - create(data: IStoreRequest): Observable { - return this.http.post(this.apiRoutes.list(), data); - } -} diff --git a/src/app/modules/stores/views/index.ts b/src/app/modules/stores/views/index.ts deleted file mode 100644 index bc9dfc9..0000000 --- a/src/app/modules/stores/views/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './list.component'; -export * from './single.component'; diff --git a/src/app/modules/stores/views/list.component.html b/src/app/modules/stores/views/list.component.html deleted file mode 100644 index a077024..0000000 --- a/src/app/modules/stores/views/list.component.html +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/app/modules/stores/views/list.component.ts b/src/app/modules/stores/views/list.component.ts deleted file mode 100644 index 3b99703..0000000 --- a/src/app/modules/stores/views/list.component.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { - IColumn, - PageDataListComponent, -} from '@/shared/components/pageDataList/page-data-list.component'; -import { Component, signal } from '@angular/core'; -import { StoreFormComponent } from '../components/form.component'; -import { IStoreResponse } from '../models'; -import { StoresService } from '../services/main.service'; - -@Component({ - selector: 'app-stores', - templateUrl: './list.component.html', - imports: [PageDataListComponent, StoreFormComponent], -}) -export class StoresComponent { - constructor(private storeService: StoresService) { - this.getData(); - } - - columns = [ - { field: 'id', header: 'شناسه' }, - { field: 'name', header: 'نام' }, - { field: 'location', header: 'موقعیت' }, - { field: 'isActive', header: 'فعال' }, - { field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' }, - { field: 'updatedAt', header: 'تاریخ به‌روزرسانی', type: 'date' }, - , - ] as IColumn[]; - - loading = signal(false); - items = signal([]); - visibleForm = signal(false); - - refresh() { - this.getData(); - } - - getData() { - this.loading.set(true); - this.storeService.getAll().subscribe((res) => { - this.loading.set(false); - this.items.set(res.data); - }); - } - - openAddForm() { - this.visibleForm.set(true); - } -} diff --git a/src/app/modules/stores/views/single.component.html b/src/app/modules/stores/views/single.component.html deleted file mode 100644 index 7144e26..0000000 --- a/src/app/modules/stores/views/single.component.html +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/src/app/modules/stores/views/single.component.ts b/src/app/modules/stores/views/single.component.ts deleted file mode 100644 index 95581fd..0000000 --- a/src/app/modules/stores/views/single.component.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-store', - templateUrl: './single.component.html', -}) -export class StoreComponent { - constructor() {} -} diff --git a/src/app/modules/suppliers/components/invoices/invoices.component.html b/src/app/modules/suppliers/components/invoices/invoices.component.html index e5ec3df..2389283 100644 --- a/src/app/modules/suppliers/components/invoices/invoices.component.html +++ b/src/app/modules/suppliers/components/invoices/invoices.component.html @@ -2,7 +2,14 @@
فاکتورهای خرید - +
+ @if (showViewAllCTA) { + + } + +
@@ -13,7 +20,10 @@ شناسه رسید انبار مجموع قیمت + وضعیت تسویه حساب + مانده حساب تاریخ + @@ -32,7 +42,18 @@ {{ item.code }} {{ item.inventory.name }} + + + + + diff --git a/src/app/modules/suppliers/components/invoices/invoices.component.ts b/src/app/modules/suppliers/components/invoices/invoices.component.ts index 5c64347..8d2ae32 100644 --- a/src/app/modules/suppliers/components/invoices/invoices.component.ts +++ b/src/app/modules/suppliers/components/invoices/invoices.component.ts @@ -1,4 +1,5 @@ import { Maybe } from '@/core'; +import { CatalogPurchaseReceiptStatusTagComponent } from '@/shared/catalog'; import { JalaliDateDirective, PriceMaskDirective } from '@/shared/directives'; import { CommonModule } from '@angular/common'; import { Component, Input, OnInit, signal } from '@angular/core'; @@ -7,8 +8,8 @@ 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'; +import { ISupplierInvoicesResponse } from '../../models/invoices.io'; +import { SupplierInvoicesService } from '../../services'; @Component({ selector: 'suppliers-invoices', @@ -23,12 +24,16 @@ import { SuppliersService } from '../../services/main.service'; Ripple, Button, RouterLink, + CatalogPurchaseReceiptStatusTagComponent, ], templateUrl: './invoices.component.html', }) export class SuppliersInvoicesComponent implements OnInit { @Input() supplierId!: string; - constructor(private service: SuppliersService) {} + @Input() showViewAllCTA: boolean = true; + @Input() perPage: number = 10; + + constructor(private service: SupplierInvoicesService) {} ngOnInit(): void { this.getAll(); @@ -41,7 +46,7 @@ export class SuppliersInvoicesComponent implements OnInit { getAll() { this.loading.set(true); - this.service.getInvoices(this.supplierId).subscribe({ + this.service.getAll(this.supplierId).subscribe({ next: (res) => { this.loading.set(false); this.items.set(res.data); diff --git a/src/app/modules/suppliers/components/invoices/pay-form.component.html b/src/app/modules/suppliers/components/invoices/pay-form.component.html new file mode 100644 index 0000000..f4cbce7 --- /dev/null +++ b/src/app/modules/suppliers/components/invoices/pay-form.component.html @@ -0,0 +1,23 @@ + +
+ {{ this.debtAmount }} + + + + + +
diff --git a/src/app/modules/suppliers/components/invoices/pay-form.component.ts b/src/app/modules/suppliers/components/invoices/pay-form.component.ts new file mode 100644 index 0000000..9f2254e --- /dev/null +++ b/src/app/modules/suppliers/components/invoices/pay-form.component.ts @@ -0,0 +1,57 @@ +import { InventoryBankAccountSelectComponent } from '@/modules/inventories/components/bankAccounts/select.component'; +import { PaymentMethodTypeSelectComponent } from '@/shared/catalog/paymentMethodTypes/components'; +import { PaymentType } from '@/shared/catalog/paymentTypes'; +import { InputComponent } from '@/shared/components'; +import { AbstractFormDialog } from '@/shared/components/abstract-form-dialog.component'; +import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component'; +import { formatNumber } from '@/utils/price-mask.utils'; +import { Component, computed, inject, Input } from '@angular/core'; +import { ReactiveFormsModule, Validators } from '@angular/forms'; +import dayjs from 'dayjs'; +import { Dialog } from 'primeng/dialog'; +import { IPayRequestPayload, IPayResponse } from '../../models/invoices.io'; +import { SupplierInvoicesService } from '../../services'; + +@Component({ + selector: 'app-supplier-invoice-pay-form', + templateUrl: './pay-form.component.html', + imports: [ + Dialog, + ReactiveFormsModule, + InventoryBankAccountSelectComponent, + FormFooterActionsComponent, + InputComponent, + PaymentMethodTypeSelectComponent, + ], +}) +export class SupplierInvoicePayFormComponent extends AbstractFormDialog< + IPayResponse, + IPayRequestPayload +> { + @Input() debtAmount!: number; + @Input() supplierId!: string; + @Input() invoiceId!: string; + @Input() inventoryId!: string; + + private readonly service = inject(SupplierInvoicesService); + + override defaultValues = { + type: PaymentType.PAYMENT, + payedAt: dayjs().format('YYYY-MM-DD'), + }; + + form = this.fb.group({ + amount: [0, [Validators.required, Validators.max(this.debtAmount), Validators.min(1)]], + bankAccountId: [null, [Validators.required]], + paymentMethod: [null, [Validators.required]], + type: [PaymentType.PAYMENT, [Validators.required]], + payedAt: [dayjs().format('YYYY-MM-DD'), [Validators.required]], + description: [''], + }); + + amountFieldHint = computed(() => `بدهی شما مبلغ ${formatNumber(this.debtAmount)} ریال می‌باشد.`); + + submitForm(payload: IPayRequestPayload) { + return this.service.pay(this.supplierId, this.invoiceId, payload); + } +} diff --git a/src/app/modules/suppliers/components/ledger/ledger.component.html b/src/app/modules/suppliers/components/ledger/ledger.component.html new file mode 100644 index 0000000..e3fd9c9 --- /dev/null +++ b/src/app/modules/suppliers/components/ledger/ledger.component.html @@ -0,0 +1,41 @@ + + +
+ تراکنش‌های تامین‌کننده +
+ @if (showViewAllCTA) { + + } + +
+
+
+ + + + + ردیف + بدهکار + بستانکار + تسویه + تاریخ + + + + + {{ i + 1 }} + + + + + + + + + + هیچ فاکتوری ثبت نشده است. + + + + +
diff --git a/src/app/modules/suppliers/components/ledger/ledger.component.ts b/src/app/modules/suppliers/components/ledger/ledger.component.ts new file mode 100644 index 0000000..6962f3d --- /dev/null +++ b/src/app/modules/suppliers/components/ledger/ledger.component.ts @@ -0,0 +1,56 @@ +import { Maybe } from '@/core'; +import { CatalogPurchaseReceiptStatusTagComponent } from '@/shared/catalog'; +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 { ButtonDirective } from 'primeng/button'; +import { Card } from 'primeng/card'; +import { TableModule } from 'primeng/table'; +import { ISupplierLedgerResponse } from '../../models'; +import { SuppliersService } from '../../services'; + +@Component({ + selector: 'supplier-ledger', + standalone: true, + imports: [ + CommonModule, + Card, + ButtonDirective, + TableModule, + PriceMaskDirective, + JalaliDateDirective, + RouterLink, + CatalogPurchaseReceiptStatusTagComponent, + ], + templateUrl: './ledger.component.html', +}) +export class SupplierLedgerComponent implements OnInit { + @Input() supplierId!: string; + @Input() showViewAllCTA: boolean = true; + @Input() perPage: number = 10; + + constructor(private service: SuppliersService) {} + + ngOnInit(): void { + this.getAll(); + } + + expandedRows = {}; + + loading = signal(true); + items = signal>(null); + + getAll() { + this.loading.set(true); + this.service.getLedger(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 7b8fe1c..1279885 100644 --- a/src/app/modules/suppliers/constants/apiRoutes/index.ts +++ b/src/app/modules/suppliers/constants/apiRoutes/index.ts @@ -1,9 +1,10 @@ +import { SUPPLIER_INVOICES_API_ROUTES } from './invoices'; + 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}`, + ledger: (supplierId: string) => `${baseUrl}/${supplierId}/ledger`, + invoices: SUPPLIER_INVOICES_API_ROUTES(baseUrl), }; diff --git a/src/app/modules/suppliers/constants/apiRoutes/invoices.ts b/src/app/modules/suppliers/constants/apiRoutes/invoices.ts new file mode 100644 index 0000000..a994181 --- /dev/null +++ b/src/app/modules/suppliers/constants/apiRoutes/invoices.ts @@ -0,0 +1,8 @@ +export const SUPPLIER_INVOICES_API_ROUTES = (_baseUrl: string) => { + const baseUrl = (supplierId: string) => `${_baseUrl}/${supplierId}/invoices`; + return { + list: (supplierId: string) => `${baseUrl(supplierId)}`, + single: (supplierId: string, invoiceId: string) => `${baseUrl(supplierId)}/${invoiceId}`, + pay: (supplierId: string, invoiceId: string) => `${baseUrl(supplierId)}/${invoiceId}/pay`, + }; +}; diff --git a/src/app/modules/suppliers/constants/routes/index.ts b/src/app/modules/suppliers/constants/routes/index.ts index 3ec6d6a..474af8b 100644 --- a/src/app/modules/suppliers/constants/routes/index.ts +++ b/src/app/modules/suppliers/constants/routes/index.ts @@ -1,9 +1,7 @@ import { NamedRoutes } from '@/core'; import { Routes } from '@angular/router'; -export type TSuppliersRouteNames = 'suppliers' | 'supplier'; - -export const suppliersNamedRoutes: NamedRoutes = { +export const suppliersNamedRoutes: NamedRoutes = { suppliers: { path: 'suppliers', loadComponent: () => import('../../views/list.component').then((m) => m.SuppliersComponent), @@ -20,6 +18,26 @@ export const suppliersNamedRoutes: NamedRoutes = { pagePath: () => '/suppliers/:supplierId', }, }, -}; + invoices: { + path: 'suppliers/:supplierId/invoices', + loadComponent: () => + import('../../views/invoices.component').then((m) => m.SupplierInvoicesComponent), + meta: { + title: 'فاکتورها', + pagePath: () => '/suppliers/:supplierId/invoices', + }, + }, + invoice: { + path: 'suppliers/:supplierId/invoices/:invoiceId', + loadComponent: () => + import('../../views/invoice.component').then((m) => m.SupplierInvoiceComponent), + meta: { + title: 'فاکتور', + pagePath: () => '/suppliers/:supplierId/invoices/:invoiceId', + }, + }, +} as const; + +export type TSuppliersRouteNames = (typeof suppliersNamedRoutes)[keyof typeof suppliersNamedRoutes]; export const SUPPLIERS_ROUTES: Routes = Object.values(suppliersNamedRoutes); diff --git a/src/app/modules/suppliers/models/invoices.io.ts b/src/app/modules/suppliers/models/invoices.io.ts new file mode 100644 index 0000000..93a00a8 --- /dev/null +++ b/src/app/modules/suppliers/models/invoices.io.ts @@ -0,0 +1,39 @@ +import ISummary from '@/core/models/summary'; +import { PurchaseReceiptStatus } from '@/shared/catalog'; +import { PaymentMethodType } from '@/shared/catalog/paymentMethodTypes'; +import { PaymentType } from '@/shared/catalog/paymentTypes'; +import { ISupplierReceiptItemSummary } from './types'; + +export interface ISupplierInvoicesRawResponse { + id: number; + code: string; + totalAmount: string; + paidAmount: string; + status: PurchaseReceiptStatus; + description?: string; + createdAt: string; + updatedAt: string; + items: ISupplierReceiptItemSummary[]; + inventory: ISummary; + payments: any[]; +} + +export interface ISupplierInvoicesResponse extends ISupplierInvoicesRawResponse {} + +export interface IPayRequestPayload { + amount: number; + bankAccountId: string; + paymentMethod: PaymentMethodType; + type: PaymentType; + payedAt: string; + description?: string; +} +export interface IPayRawResponse { + amount: number; + bankAccountId: string; + paymentMethod: PaymentMethodType; + type: PaymentType; + payedAt: string; + description?: string; +} +export interface IPayResponse extends IPayRawResponse {} diff --git a/src/app/modules/suppliers/models/io.d.ts b/src/app/modules/suppliers/models/io.d.ts index 19eab7f..5324a7a 100644 --- a/src/app/modules/suppliers/models/io.d.ts +++ b/src/app/modules/suppliers/models/io.d.ts @@ -1,4 +1,4 @@ -import { IReceiptsOverview, ISupplierInventorySummary, ISupplierReceiptItemSummary } from './types'; +import { IReceiptsOverview } from './types'; export interface ISupplierRawResponse { id: number; @@ -39,15 +39,5 @@ export interface ISupplierRequest { 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 {} +export interface ISupplierLedgerRawResponse {} +export interface ISupplierLedgerResponse extends ISupplierLedgerRawResponse {} diff --git a/src/app/modules/suppliers/services/index.ts b/src/app/modules/suppliers/services/index.ts new file mode 100644 index 0000000..b4e7e7b --- /dev/null +++ b/src/app/modules/suppliers/services/index.ts @@ -0,0 +1,2 @@ +export * from './invoices.service'; +export * from './main.service'; diff --git a/src/app/modules/suppliers/services/invoices.service.ts b/src/app/modules/suppliers/services/invoices.service.ts new file mode 100644 index 0000000..5aa8673 --- /dev/null +++ b/src/app/modules/suppliers/services/invoices.service.ts @@ -0,0 +1,39 @@ +import { IPaginatedResponse } from '@/core/models/service.model'; +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { SUPPLIERS_API_ROUTES } from '../constants'; +import { + IPayRawResponse, + IPayRequestPayload, + IPayResponse, + ISupplierInvoicesRawResponse, + ISupplierInvoicesResponse, +} from '../models/invoices.io'; + +@Injectable({ providedIn: 'root' }) +export class SupplierInvoicesService { + constructor(private http: HttpClient) {} + + private apiRoutes = SUPPLIERS_API_ROUTES.invoices; + + getAll(supplierId: string): Observable> { + return this.http.get>( + this.apiRoutes.list(supplierId), + ); + } + + getSingle(supplierId: string, invoiceId: string): Observable { + return this.http.get( + this.apiRoutes.single(supplierId, invoiceId), + ); + } + + pay( + supplierId: string, + invoiceId: string, + payload: IPayRequestPayload, + ): Observable { + return this.http.post(this.apiRoutes.pay(supplierId, invoiceId), payload); + } +} diff --git a/src/app/modules/suppliers/services/main.service.ts b/src/app/modules/suppliers/services/main.service.ts index 924f808..0fbd35b 100644 --- a/src/app/modules/suppliers/services/main.service.ts +++ b/src/app/modules/suppliers/services/main.service.ts @@ -7,8 +7,7 @@ import { SUPPLIERS_API_ROUTES } from '../constants'; import { ISupplierFullInfoRawResponse, ISupplierFullInfoResponse, - ISupplierInvoicesRawResponse, - ISupplierInvoicesResponse, + ISupplierLedgerResponse, ISupplierRawResponse, ISupplierRequest, ISupplierResponse, @@ -50,15 +49,9 @@ export class SuppliersService { ); } - 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), + getLedger(supplierId: string): Observable> { + return this.http.get>( + this.apiRoutes.ledger(supplierId), ); } } diff --git a/src/app/modules/suppliers/store/index.ts b/src/app/modules/suppliers/store/index.ts new file mode 100644 index 0000000..e48d1e2 --- /dev/null +++ b/src/app/modules/suppliers/store/index.ts @@ -0,0 +1,2 @@ +export * from './invoice.store'; +export * from './main.store'; diff --git a/src/app/modules/suppliers/store/invoice.store.ts b/src/app/modules/suppliers/store/invoice.store.ts new file mode 100644 index 0000000..af32110 --- /dev/null +++ b/src/app/modules/suppliers/store/invoice.store.ts @@ -0,0 +1,78 @@ +import { ToastService } from '@/core/services/toast.service'; +import { EntityState, EntityStore } from '@/core/state'; +import { Injectable, InjectionToken } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { ISupplierInvoicesResponse } from '../models/invoices.io'; +import { SupplierInvoicesService } from '../services'; + +export interface SupplierInvoiceState extends EntityState {} +export const SUPPLIER_ID = new InjectionToken('SUPPLIER_ID'); + +@Injectable({ + providedIn: 'root', +}) +export class SupplierInvoiceStore extends EntityStore< + ISupplierInvoicesResponse, + SupplierInvoiceState +> { + private readonly _inventoryState = { + isRefreshing: false, + }; + + constructor( + private activeRoute: ActivatedRoute, + private service: SupplierInvoicesService, + private toastService: ToastService, + ) { + super({ + loading: false, + initialized: false, + error: null, + isRefreshing: false, + entities: {}, + selectedId: null, + ids: [], + }); + this.initial(); + } + + supplierId!: string; + + initial() {} + + getSingle(invoiceId: string) { + if (this.entities()[invoiceId]) { + return; + } + this.patchState({ loading: true }); + this.service.getSingle(this.supplierId, invoiceId).subscribe({ + next: (res) => { + this.patchState({ + entities: { [res.id]: res }, + loading: false, + }); + }, + error: (err) => { + this.patchState({ loading: false, error: err }); + this.toastService.error({ + text: 'خطا در دریافت اطلاعات فاکتور', + }); + }, + }); + } + + refreshSingle(supplierId: string) {} + + 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/suppliers/store/invoices.store.ts b/src/app/modules/suppliers/store/invoices.store.ts new file mode 100644 index 0000000..684ffc1 --- /dev/null +++ b/src/app/modules/suppliers/store/invoices.store.ts @@ -0,0 +1,93 @@ +import { ToastService } from '@/core/services/toast.service'; +import { PaginatedState, PaginatedStore } from '@/core/state'; +import { Injectable, InjectionToken } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { ISupplierInvoicesResponse } from '../models/invoices.io'; +import { SupplierInvoicesService } from '../services'; + +export interface SupplierInvoicesState extends PaginatedState {} +export const SUPPLIER_ID = new InjectionToken('SUPPLIER_ID'); + +@Injectable({ + providedIn: 'root', +}) +export class SupplierInvoicesStore extends PaginatedStore< + ISupplierInvoicesResponse, + SupplierInvoicesState +> { + private readonly _inventoryState = { + isRefreshing: false, + }; + + constructor( + private activeRoute: ActivatedRoute, + private service: SupplierInvoicesService, + private toastService: ToastService, + ) { + super({ + loading: false, + initialized: false, + error: null, + isRefreshing: false, + items: [], + totalCount: 0, + currentPage: 0, + pageSize: 0, + totalPages: 0, + hasMore: false, + }); + this.initial(); + } + + supplierId!: string; + + initial() { + console.log(this.supplierId); + + if (!this.initialized()) { + this.getData(); + } + } + + getData() { + this.patchState({ loading: true }); + this.service.getAll(this.supplierId).subscribe({ + next: (res) => { + this.patchState({ + items: res.data, + totalCount: res.meta.totalRecords, + currentPage: res.meta.page, + pageSize: res.meta.perPage, + totalPages: res.meta.totalPages, + hasMore: res.meta.page < res.meta.totalPages, + initialized: true, + loading: false, + }); + }, + error: (err) => { + this.patchState({ loading: false, error: err }); + this.toastService.error({ + text: 'خطا در دریافت اطلاعات فاکتور', + }); + }, + }); + } + + refreshSingle(supplierId: string) {} + + reset(): void { + const queryParams = this.activeRoute.snapshot.queryParams; + this.setState({ + loading: false, + initialized: false, + error: null, + isRefreshing: false, + items: [], + totalCount: 0, + currentPage: 0, + pageSize: 0, + totalPages: 0, + hasMore: false, + }); + } +} diff --git a/src/app/modules/suppliers/store/main.store.ts b/src/app/modules/suppliers/store/main.store.ts new file mode 100644 index 0000000..232ed83 --- /dev/null +++ b/src/app/modules/suppliers/store/main.store.ts @@ -0,0 +1,72 @@ +import { ToastService } from '@/core/services/toast.service'; +import { EntityState, EntityStore } from '@/core/state'; +import { Injectable } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { ISupplierFullInfoResponse } from '../models'; +import { SuppliersService } from '../services'; + +export interface SupplierState extends EntityState {} + +@Injectable({ + providedIn: 'root', +}) +export class SupplierStore extends EntityStore { + private readonly _inventoryState = { + isRefreshing: false, + }; + + constructor( + private activeRoute: ActivatedRoute, + private service: SuppliersService, + private toastService: ToastService, + ) { + super({ + loading: false, + initialized: false, + error: null, + isRefreshing: false, + entities: {}, + selectedId: null, + ids: [], + }); + this.initial(); + } + + initial() {} + + getSingle(supplierId: string) { + if (this.entities()[supplierId]) { + return; + } + this.patchState({ loading: true }); + this.service.getSingle(supplierId).subscribe({ + next: (res) => { + this.patchState({ + entities: { [res.id]: res }, + loading: false, + }); + }, + error: (err) => { + this.patchState({ loading: false, error: err }); + this.toastService.error({ + text: 'خطا در دریافت اطلاعات انبار', + }); + }, + }); + } + + refreshSingle(supplierId: string) {} + + 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/suppliers/views/invoice.component.html b/src/app/modules/suppliers/views/invoice.component.html new file mode 100644 index 0000000..7d3e4c3 --- /dev/null +++ b/src/app/modules/suppliers/views/invoice.component.html @@ -0,0 +1,33 @@ +
+ + + + + + + +
+
+ +
+
+ +
+
+ + @if (invoice()?.status) { + + } + +
+
+ @if (invoice() && supplier()) { + + } +
diff --git a/src/app/modules/suppliers/views/invoice.component.ts b/src/app/modules/suppliers/views/invoice.component.ts new file mode 100644 index 0000000..526f6bd --- /dev/null +++ b/src/app/modules/suppliers/views/invoice.component.ts @@ -0,0 +1,58 @@ +import { CatalogPurchaseReceiptStatusTagComponent } from '@/shared/catalog'; +import { KeyValueComponent } from '@/shared/components'; +import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component'; +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 { SupplierInvoiceStore, SupplierStore } from '../store'; + +@Component({ + selector: 'app-supplier-invoice', + templateUrl: './invoice.component.html', + imports: [ + KeyValueComponent, + InnerPagesHeaderComponent, + CatalogPurchaseReceiptStatusTagComponent, + ButtonDirective, + SupplierInvoicePayFormComponent, + ], +}) +export class SupplierInvoiceComponent { + private route = inject(ActivatedRoute); + private supplierStore = inject(SupplierStore); + private store = inject(SupplierInvoiceStore); + + constructor() { + this.supplierStore.getSingle(this.supplierId); + this.store.supplierId = this.supplierId; + this.store.getSingle(this.invoiceId); + } + + supplierId = this.route.snapshot.params['supplierId']; + invoiceId = this.route.snapshot.params['invoiceId']; + + showPayForm = signal(false); + + supplier = computed(() => this.supplierStore.entities()[this.supplierId]); + supplierLoading = this.supplierStore.loading; + + invoice = computed(() => this.store.entities()[this.invoiceId]); + loading = this.store.loading; + + pageTitle = computed(() => { + return 'فاکتور شماره ' + this.invoice()?.code; + }); + + debtAmount = computed(() => { + const invoice = this.invoice(); + if (!invoice) { + return 0; + } + return parseFloat(invoice.totalAmount) - parseFloat(invoice.paidAmount); + }); + + openPayForm() { + this.showPayForm.set(true); + } +} diff --git a/src/app/modules/suppliers/views/invoices.component.html b/src/app/modules/suppliers/views/invoices.component.html new file mode 100644 index 0000000..9c284fc --- /dev/null +++ b/src/app/modules/suppliers/views/invoices.component.html @@ -0,0 +1,16 @@ +
+ +
+
+ +
+
+
+
+ @for (item of topBarCardDetails; track $index) { + + } +
+
+ +
diff --git a/src/app/modules/suppliers/views/invoices.component.ts b/src/app/modules/suppliers/views/invoices.component.ts new file mode 100644 index 0000000..70181e4 --- /dev/null +++ b/src/app/modules/suppliers/views/invoices.component.ts @@ -0,0 +1,50 @@ +import { KeyValueComponent } from '@/shared/components'; +import { DetailsInfoCardComponent } from '@/shared/components/detailsInfoCard/details-info-card.component'; +import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component'; +import { formatWithCurrency } from '@/utils'; +import { Component, computed, inject } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { SuppliersInvoicesComponent } from '../components/invoices/invoices.component'; +import { SupplierStore } from '../store'; + +@Component({ + selector: 'app-supplier-invoices', + templateUrl: './invoices.component.html', + imports: [ + KeyValueComponent, + SuppliersInvoicesComponent, + InnerPagesHeaderComponent, + DetailsInfoCardComponent, + ], +}) +export class SupplierInvoicesComponent { + private route = inject(ActivatedRoute); + private supplierStore = inject(SupplierStore); + constructor() { + this.supplierStore.getSingle(this.supplierId); + } + + supplierId = this.route.snapshot.params['supplierId']; + invoiceId = this.route.snapshot.params['invoiceId']; + + supplier = computed(() => this.supplierStore.entities()[this.supplierId]); + + supplierLoading = this.supplierStore.loading(); + + pageTitle = computed(() => `فاکتورهای ${this.supplier()?.fullname || ''}`); + + get topBarCardDetails() { + return [ + { + icon: 'pi pi-box', + label: 'تعداد فاکتورهای صادر شده', + value: this.supplier()?.receiptsOverview.count, + }, + { + icon: 'pi pi-dollar', + label: 'مجموع کل فاکتورها', + value: formatWithCurrency(this.supplier()?.receiptsOverview.totalPayment || 0), + }, + ]; + } +} diff --git a/src/app/modules/suppliers/views/list.component.html b/src/app/modules/suppliers/views/list.component.html index 5a656bc..bb4d362 100644 --- a/src/app/modules/suppliers/views/list.component.html +++ b/src/app/modules/suppliers/views/list.component.html @@ -8,6 +8,7 @@ [items]="items()" [loading]="loading()" [showDetails]="true" + (onDetails)="toDetails($event)" (onAdd)="openAddForm()" /> diff --git a/src/app/modules/suppliers/views/list.component.ts b/src/app/modules/suppliers/views/list.component.ts index 96121e4..06a3c59 100644 --- a/src/app/modules/suppliers/views/list.component.ts +++ b/src/app/modules/suppliers/views/list.component.ts @@ -3,6 +3,7 @@ import { PageDataListComponent, } from '@/shared/components/pageDataList/page-data-list.component'; import { Component, signal } from '@angular/core'; +import { Router } from '@angular/router'; import { SupplierFormComponent } from '../components/form.component'; import { ISupplierResponse } from '../models'; import { SuppliersService } from '../services/main.service'; @@ -13,7 +14,10 @@ import { SuppliersService } from '../services/main.service'; imports: [PageDataListComponent, SupplierFormComponent], }) export class SuppliersComponent { - constructor(private supplierService: SuppliersService) { + constructor( + private supplierService: SuppliersService, + private router: Router, + ) { this.getData(); } @@ -57,4 +61,8 @@ export class SuppliersComponent { openAddForm() { this.visibleForm.set(true); } + + toDetails(supplier: ISupplierResponse) { + this.router.navigate(['/suppliers', supplier.id]); + } } diff --git a/src/app/modules/suppliers/views/single.component.html b/src/app/modules/suppliers/views/single.component.html index 27cee4f..e393b0c 100644 --- a/src/app/modules/suppliers/views/single.component.html +++ b/src/app/modules/suppliers/views/single.component.html @@ -26,56 +26,7 @@ - +
+ +
diff --git a/src/app/modules/suppliers/views/single.component.ts b/src/app/modules/suppliers/views/single.component.ts index 2174fa6..b9b2206 100644 --- a/src/app/modules/suppliers/views/single.component.ts +++ b/src/app/modules/suppliers/views/single.component.ts @@ -6,6 +6,7 @@ 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 { SupplierLedgerComponent } from '../components/ledger/ledger.component'; import { ISupplierFullInfoResponse } from '../models'; import { SuppliersService } from '../services/main.service'; @@ -19,6 +20,7 @@ import { SuppliersService } from '../services/main.service'; RouterLink, ButtonDirective, SuppliersInvoicesComponent, + SupplierLedgerComponent, ], }) export class SupplierComponent { @@ -49,12 +51,12 @@ export class SupplierComponent { return [ { icon: 'pi pi-box', - label: 'تعداد فاکتورها', + label: 'تعداد فاکتورهای صادر شده', value: this.data()?.receiptsOverview.count, }, { icon: 'pi pi-dollar', - label: 'ارزش کلی پرداختی‌ها', + label: 'مجموع کل فاکتورها', value: formatWithCurrency(this.data()?.receiptsOverview.totalPayment || 0), }, ]; diff --git a/src/app/shared/catalog/banks/store/main.store.ts b/src/app/shared/catalog/banks/store/main.store.ts index abf3e6b..0563b7c 100644 --- a/src/app/shared/catalog/banks/store/main.store.ts +++ b/src/app/shared/catalog/banks/store/main.store.ts @@ -5,7 +5,9 @@ import { catchError, map } from 'rxjs/operators'; import { IBankResponse } from '../models'; import { BanksService } from '../services/main.service'; -export interface banksState extends BaseState {} +export interface banksState extends BaseState { + items: IBankResponse[]; +} @Injectable({ providedIn: 'root', diff --git a/src/app/shared/catalog/index.ts b/src/app/shared/catalog/index.ts index 84a260b..7ec7667 100644 --- a/src/app/shared/catalog/index.ts +++ b/src/app/shared/catalog/index.ts @@ -1,2 +1,3 @@ export * from './gender'; export * from './movementTypes'; +export * from './purchaseReceiptStatus'; diff --git a/src/app/shared/catalog/paymentMethodTypes/components/index.ts b/src/app/shared/catalog/paymentMethodTypes/components/index.ts new file mode 100644 index 0000000..162984a --- /dev/null +++ b/src/app/shared/catalog/paymentMethodTypes/components/index.ts @@ -0,0 +1,2 @@ +export * from './select/select.component'; +export * from './tag/tag.component'; diff --git a/src/app/shared/catalog/paymentMethodTypes/components/select/select.component.html b/src/app/shared/catalog/paymentMethodTypes/components/select/select.component.html new file mode 100644 index 0000000..c88f71c --- /dev/null +++ b/src/app/shared/catalog/paymentMethodTypes/components/select/select.component.html @@ -0,0 +1,13 @@ + + + + diff --git a/src/app/shared/catalog/paymentMethodTypes/components/select/select.component.ts b/src/app/shared/catalog/paymentMethodTypes/components/select/select.component.ts new file mode 100644 index 0000000..eea3af3 --- /dev/null +++ b/src/app/shared/catalog/paymentMethodTypes/components/select/select.component.ts @@ -0,0 +1,28 @@ +import { Maybe } from '@/core'; +import { UikitFieldComponent } from '@/uikit'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { Select } from 'primeng/select'; +import { PaymentMethodType } from '../../types'; + +@Component({ + selector: 'app-catalog-payment-method-type-select-field', + templateUrl: './select.component.html', + imports: [ReactiveFormsModule, Select, UikitFieldComponent], +}) +export class PaymentMethodTypeSelectComponent { + @Input() control = new FormControl>(null); + @Input() showLabel: boolean = true; + @Input() showErrors: boolean = true; + @Input() showClear: boolean = false; + @Input() isFullDataOptionValue: boolean = false; + @Output() onChange = new EventEmitter>(); + + items = [ + { label: 'نقدی', value: 'CASH' }, + { label: 'کارت بانکی', value: 'CARD' }, + { label: 'حساب', value: 'BANK' }, + { label: 'چک', value: 'CHECK' }, + { label: 'سایر', value: 'OTHER' }, + ] as { label: string; value: PaymentMethodType }[]; +} diff --git a/src/app/shared/catalog/paymentMethodTypes/components/tag/tag.component.html b/src/app/shared/catalog/paymentMethodTypes/components/tag/tag.component.html new file mode 100644 index 0000000..d481d6b --- /dev/null +++ b/src/app/shared/catalog/paymentMethodTypes/components/tag/tag.component.html @@ -0,0 +1,3 @@ + + {{ text }} + diff --git a/src/app/shared/catalog/paymentMethodTypes/components/tag/tag.component.ts b/src/app/shared/catalog/paymentMethodTypes/components/tag/tag.component.ts new file mode 100644 index 0000000..252731d --- /dev/null +++ b/src/app/shared/catalog/paymentMethodTypes/components/tag/tag.component.ts @@ -0,0 +1,21 @@ +import { Component, Input } from '@angular/core'; +import { Tag } from 'primeng/tag'; +import { PaymentMethodType } from '../../types'; +import { getPaymentMethodTypeColor, getPaymentMethodTypeText } from '../../utils'; + +@Component({ + selector: 'app-catalog-payment-method-type-tag', + templateUrl: './tag.component.html', + imports: [Tag], +}) +export class CatalogPaymentMethodTypeTagComponent { + @Input() type!: PaymentMethodType; + constructor() {} + + get text() { + return getPaymentMethodTypeText(this.type); + } + get color() { + return getPaymentMethodTypeColor(this.type); + } +} diff --git a/src/app/shared/catalog/paymentMethodTypes/index.ts b/src/app/shared/catalog/paymentMethodTypes/index.ts new file mode 100644 index 0000000..9d922da --- /dev/null +++ b/src/app/shared/catalog/paymentMethodTypes/index.ts @@ -0,0 +1,3 @@ +export * from './components/tag/tag.component'; +export * from './types'; +export * from './utils'; diff --git a/src/app/shared/catalog/paymentMethodTypes/types.ts b/src/app/shared/catalog/paymentMethodTypes/types.ts new file mode 100644 index 0000000..6fb954f --- /dev/null +++ b/src/app/shared/catalog/paymentMethodTypes/types.ts @@ -0,0 +1,9 @@ +export const PaymentMethodType = { + CASH: 'CASH', + CARD: 'CARD', + BANK: 'BANK', + CHECK: 'CHECK', + OTHER: 'OTHER', +} as const; + +export type PaymentMethodType = (typeof PaymentMethodType)[keyof typeof PaymentMethodType]; diff --git a/src/app/shared/catalog/paymentMethodTypes/utils.ts b/src/app/shared/catalog/paymentMethodTypes/utils.ts new file mode 100644 index 0000000..f577c56 --- /dev/null +++ b/src/app/shared/catalog/paymentMethodTypes/utils.ts @@ -0,0 +1,32 @@ +import { TagSeverity } from '@/core/models/tag'; +import { PaymentMethodType } from './types'; + +export const getPaymentMethodTypeText = (value: PaymentMethodType): string => { + switch (value) { + case PaymentMethodType.CASH: + return 'نقدی'; + case PaymentMethodType.CARD: + return 'کارت'; + case PaymentMethodType.BANK: + return 'بانک'; + case PaymentMethodType.CHECK: + return 'چک'; + case PaymentMethodType.OTHER: + return 'سایر'; + } +}; + +export const getPaymentMethodTypeColor = (value: PaymentMethodType): TagSeverity => { + switch (value) { + case PaymentMethodType.CASH: + return 'danger'; + case PaymentMethodType.CARD: + return 'info'; + case PaymentMethodType.BANK: + return 'success'; + case PaymentMethodType.CHECK: + return 'warn'; + case PaymentMethodType.OTHER: + return 'secondary'; + } +}; diff --git a/src/app/shared/catalog/paymentTypes/components/index.ts b/src/app/shared/catalog/paymentTypes/components/index.ts new file mode 100644 index 0000000..b544a9c --- /dev/null +++ b/src/app/shared/catalog/paymentTypes/components/index.ts @@ -0,0 +1 @@ +export * from './tag/tag.component'; diff --git a/src/app/shared/catalog/paymentTypes/components/tag/tag.component.html b/src/app/shared/catalog/paymentTypes/components/tag/tag.component.html new file mode 100644 index 0000000..d481d6b --- /dev/null +++ b/src/app/shared/catalog/paymentTypes/components/tag/tag.component.html @@ -0,0 +1,3 @@ + + {{ text }} + diff --git a/src/app/shared/catalog/paymentTypes/components/tag/tag.component.ts b/src/app/shared/catalog/paymentTypes/components/tag/tag.component.ts new file mode 100644 index 0000000..18548cc --- /dev/null +++ b/src/app/shared/catalog/paymentTypes/components/tag/tag.component.ts @@ -0,0 +1,21 @@ +import { Component, Input } from '@angular/core'; +import { Tag } from 'primeng/tag'; +import { PaymentType } from '../../types'; +import { getPaymentTypeColor, getPaymentTypeText } from '../../utils'; + +@Component({ + selector: 'app-catalog-payment-type-tag', + templateUrl: './tag.component.html', + imports: [Tag], +}) +export class CatalogPaymentTypeTagComponent { + @Input() type!: PaymentType; + constructor() {} + + get text() { + return getPaymentTypeText(this.type); + } + get color() { + return getPaymentTypeColor(this.type); + } +} diff --git a/src/app/shared/catalog/paymentTypes/index.ts b/src/app/shared/catalog/paymentTypes/index.ts new file mode 100644 index 0000000..9d922da --- /dev/null +++ b/src/app/shared/catalog/paymentTypes/index.ts @@ -0,0 +1,3 @@ +export * from './components/tag/tag.component'; +export * from './types'; +export * from './utils'; diff --git a/src/app/shared/catalog/paymentTypes/types.ts b/src/app/shared/catalog/paymentTypes/types.ts new file mode 100644 index 0000000..85f60ed --- /dev/null +++ b/src/app/shared/catalog/paymentTypes/types.ts @@ -0,0 +1,6 @@ +export const PaymentType = { + PAYMENT: 'PAYMENT', + REFUND: 'REFUND', +} as const; + +export type PaymentType = (typeof PaymentType)[keyof typeof PaymentType]; diff --git a/src/app/shared/catalog/paymentTypes/utils.ts b/src/app/shared/catalog/paymentTypes/utils.ts new file mode 100644 index 0000000..8053171 --- /dev/null +++ b/src/app/shared/catalog/paymentTypes/utils.ts @@ -0,0 +1,20 @@ +import { TagSeverity } from '@/core/models/tag'; +import { PaymentType } from './types'; + +export const getPaymentTypeText = (value: PaymentType): string => { + switch (value) { + case PaymentType.PAYMENT: + return 'پرداخت'; + case PaymentType.REFUND: + return 'بازگشت وجه'; + } +}; + +export const getPaymentTypeColor = (value: PaymentType): TagSeverity => { + switch (value) { + case PaymentType.PAYMENT: + return 'success'; + case PaymentType.REFUND: + return 'warn'; + } +}; diff --git a/src/app/shared/catalog/purchaseReceiptStatus/index.ts b/src/app/shared/catalog/purchaseReceiptStatus/index.ts new file mode 100644 index 0000000..7d1f2fb --- /dev/null +++ b/src/app/shared/catalog/purchaseReceiptStatus/index.ts @@ -0,0 +1,3 @@ +export * from './tag.component'; +export * from './types'; +export * from './utils'; diff --git a/src/app/shared/catalog/purchaseReceiptStatus/tag.component.html b/src/app/shared/catalog/purchaseReceiptStatus/tag.component.html new file mode 100644 index 0000000..d481d6b --- /dev/null +++ b/src/app/shared/catalog/purchaseReceiptStatus/tag.component.html @@ -0,0 +1,3 @@ + + {{ text }} + diff --git a/src/app/shared/catalog/purchaseReceiptStatus/tag.component.ts b/src/app/shared/catalog/purchaseReceiptStatus/tag.component.ts new file mode 100644 index 0000000..a6e3320 --- /dev/null +++ b/src/app/shared/catalog/purchaseReceiptStatus/tag.component.ts @@ -0,0 +1,21 @@ +import { Component, Input } from '@angular/core'; +import { Tag } from 'primeng/tag'; +import { PurchaseReceiptStatus } from './types'; +import { getPurchaseReceiptStatusColor, getPurchaseReceiptStatusText } from './utils'; + +@Component({ + selector: 'app-catalog-purchase-receipt-status-tag', + templateUrl: './tag.component.html', + imports: [Tag], +}) +export class CatalogPurchaseReceiptStatusTagComponent { + @Input() type!: PurchaseReceiptStatus; + constructor() {} + + get text() { + return getPurchaseReceiptStatusText(this.type); + } + get color() { + return getPurchaseReceiptStatusColor(this.type); + } +} diff --git a/src/app/shared/catalog/purchaseReceiptStatus/types.ts b/src/app/shared/catalog/purchaseReceiptStatus/types.ts new file mode 100644 index 0000000..7e0e608 --- /dev/null +++ b/src/app/shared/catalog/purchaseReceiptStatus/types.ts @@ -0,0 +1,8 @@ +export const PurchaseReceiptStatus = { + UNPAID: 'UNPAID', + PARTIALLY_PAID: 'PARTIALLY_PAID', + PAID: 'PAID', +} as const; + +export type PurchaseReceiptStatus = + (typeof PurchaseReceiptStatus)[keyof typeof PurchaseReceiptStatus]; diff --git a/src/app/shared/catalog/purchaseReceiptStatus/utils.ts b/src/app/shared/catalog/purchaseReceiptStatus/utils.ts new file mode 100644 index 0000000..32c9669 --- /dev/null +++ b/src/app/shared/catalog/purchaseReceiptStatus/utils.ts @@ -0,0 +1,24 @@ +import { TagSeverity } from '@/core/models/tag'; +import { PurchaseReceiptStatus } from './types'; + +export const getPurchaseReceiptStatusText = (value: PurchaseReceiptStatus): string => { + switch (value) { + case PurchaseReceiptStatus.UNPAID: + return 'پرداخت نشده'; + case PurchaseReceiptStatus.PARTIALLY_PAID: + return 'پرداخت شده جزئی'; + case PurchaseReceiptStatus.PAID: + return 'تسویه شده'; + } +}; + +export const getPurchaseReceiptStatusColor = (value: PurchaseReceiptStatus): TagSeverity => { + switch (value) { + case PurchaseReceiptStatus.UNPAID: + return 'danger'; + case PurchaseReceiptStatus.PARTIALLY_PAID: + return 'warn'; + case PurchaseReceiptStatus.PAID: + return 'success'; + } +}; diff --git a/src/app/shared/components/abstract-form-dialog.component.ts b/src/app/shared/components/abstract-form-dialog.component.ts index 4780b8c..68eae76 100644 --- a/src/app/shared/components/abstract-form-dialog.component.ts +++ b/src/app/shared/components/abstract-form-dialog.component.ts @@ -8,8 +8,8 @@ import { Subscribable } from 'rxjs'; template: '', imports: [ReactiveFormsModule], }) -export abstract class AbstractFormDialog { - @Input() initialValues?: T; +export abstract class AbstractFormDialog { + @Input() initialValues?: Response; @Input() set visible(v: boolean) { @@ -21,28 +21,34 @@ export abstract class AbstractFormDialog { private visibleSignal = signal(false); @Output() visibleChange = new EventEmitter(); - @Output() onSubmit = new EventEmitter(); + @Output() onSubmit = new EventEmitter(); protected readonly fb = inject(FormBuilder); constructor(private toastService: ToastService) { effect(() => { const v = this.visibleSignal(); - if (!v) this.form.reset(); + if (!v) this.form.reset(this.defaultValues); }); } abstract form: ReturnType; + defaultValues: Partial = {}; - abstract submitForm(payload: U): Subscribable | void; + abstract submitForm(payload: Request): Subscribable | void; submitLoading = signal(false); submit() { + console.log('first'); + this.form.markAllAsTouched(); if (this.form.valid) { + console.log('isValid'); + console.log(this.submitForm); + this.form.disable(); this.submitLoading.set(true); - this.submitForm(this.form.value as U)?.subscribe({ + this.submitForm(this.form.value as Request)?.subscribe({ next: (res) => { this.toastService.success({ text: `با موفقیت ایجاد شد`, @@ -57,6 +63,11 @@ export abstract class AbstractFormDialog { this.submitLoading.set(false); this.form.enable(); }, + complete: () => { + this.submitLoading.set(false); + this.form.enable(); + console.log('end'); + }, }); } } diff --git a/src/app/shared/components/innerPagesHeader/inner-pages-header.component.html b/src/app/shared/components/innerPagesHeader/inner-pages-header.component.html new file mode 100644 index 0000000..e32cfa3 --- /dev/null +++ b/src/app/shared/components/innerPagesHeader/inner-pages-header.component.html @@ -0,0 +1,15 @@ +
+
+
+ @if (backRoute) { + + } +

{{ pageTitle }}

+
+
+ @if (actions) { +
+ +
+ } +
diff --git a/src/app/shared/components/innerPagesHeader/inner-pages-header.component.ts b/src/app/shared/components/innerPagesHeader/inner-pages-header.component.ts new file mode 100644 index 0000000..adf82ef --- /dev/null +++ b/src/app/shared/components/innerPagesHeader/inner-pages-header.component.ts @@ -0,0 +1,17 @@ +import { Maybe } from '@/core'; +import { NgTemplateOutlet } from '@angular/common'; +import { Component, ContentChild, Input, TemplateRef } from '@angular/core'; +import { RouterLink, UrlTree } from '@angular/router'; +import { Button } from 'primeng/button'; + +@Component({ + selector: 'app-inner-pages-header', + templateUrl: './inner-pages-header.component.html', + imports: [Button, RouterLink, NgTemplateOutlet], +}) +export class InnerPagesHeaderComponent { + @Input() pageTitle!: string; + @Input() backRoute?: UrlTree | string | any[]; + @ContentChild('actions', { static: true }) actions?: Maybe>; + constructor() {} +} diff --git a/src/app/shared/components/input/input.component.html b/src/app/shared/components/input/input.component.html index 56cb643..7777c61 100644 --- a/src/app/shared/components/input/input.component.html +++ b/src/app/shared/components/input/input.component.html @@ -1,6 +1,24 @@ @if (type === "switch") { + } @else if (type === "price") { + + + ریال + } @else { } + @if (hint) { + {{ hint }} + } diff --git a/src/app/shared/components/input/input.component.ts b/src/app/shared/components/input/input.component.ts index 74a33b2..3d0e50c 100644 --- a/src/app/shared/components/input/input.component.ts +++ b/src/app/shared/components/input/input.component.ts @@ -1,20 +1,41 @@ 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'; import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { InputGroupModule } from 'primeng/inputgroup'; +import { InputGroupAddon } from 'primeng/inputgroupaddon'; +import { InputNumberModule } from 'primeng/inputnumber'; import { InputTextModule } from 'primeng/inputtext'; import { ToggleSwitch } from 'primeng/toggleswitch'; @Component({ selector: 'app-input', standalone: true, - imports: [CommonModule, ReactiveFormsModule, InputTextModule, UikitFieldComponent, ToggleSwitch], + imports: [ + CommonModule, + ReactiveFormsModule, + InputTextModule, + InputGroupModule, + UikitFieldComponent, + ToggleSwitch, + PriceMaskDirective, + InputGroupAddon, + InputNumberModule, + ], templateUrl: './input.component.html', }) export class InputComponent { - @Input() type: 'simple' | 'postalCode' | 'mobile' | 'phone' | 'email' | 'checkbox' | 'switch' = - 'simple'; + @Input() type: + | 'simple' + | 'postalCode' + | 'mobile' + | 'phone' + | 'email' + | 'checkbox' + | 'switch' + | 'price' = 'simple'; @Input() control!: FormControl>; @Input() name!: string; @Input() label: string = ''; @@ -24,11 +45,12 @@ export class InputComponent { @Input() size?: 'small' | 'large'; @Input() autocomplete?: string = 'off'; @Input() showErrors = false; - @Output() valueChange = new EventEmitter(); + @Input() hint?: string; + @Output() valueChange = new EventEmitter(); onInput(ev: Event) { const v = (ev.target as HTMLInputElement).value; - this.valueChange.emit(v); + this.valueChange.emit(this.type === 'price' ? parseFloat(v) : v); } get placeholder(): string | null { @@ -42,6 +64,8 @@ export class InputComponent { return 'شماره تلفن'; case 'email': return 'آدرس ایمیل خود را وارد کنید'; + case 'price': + return 'مبلغ'; default: return ''; } @@ -52,6 +76,7 @@ export class InputComponent { case 'mobile': case 'phone': case 'postalCode': + case 'price': return 'numeric'; default: return 'text'; @@ -75,6 +100,7 @@ export class InputComponent { switch (this.type) { case 'mobile': case 'phone': + case 'price': return 'ltrInput'; case 'email': case 'postalCode':