feat: Refactor select components to use IPaginatedResponse and streamline data fetching

- Updated select components for bank accounts, bank branches, customers, inventories, product brands, product categories, products, and suppliers to extend AbstractSelectComponent with IPaginatedResponse.
- Replaced getData() method with getDataService() to return observables directly from service calls.
- Enhanced filters component to set default inventory and product selections.
- Modified inventory component to conditionally render columns based on variant type.
- Introduced InventoryStore for managing inventory state and fetching single inventory details.
- Added empty state messages in cardex and other components for better user experience.
- Updated inner pages header to use h4 for titles and improved layout in empty state component.
This commit is contained in:
2025-12-28 17:51:54 +03:30
parent 879e29f54f
commit 2583bc7ad5
26 changed files with 308 additions and 170 deletions
@@ -1,3 +1,4 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { AbstractSelectComponent } from '@/shared/components'; import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit'; import { UikitFieldComponent } from '@/uikit';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@@ -13,22 +14,16 @@ import { BankAccountFormComponent } from '../form.component';
templateUrl: './select.component.html', templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent, Button, BankAccountFormComponent], imports: [ReactiveFormsModule, Select, UikitFieldComponent, Button, BankAccountFormComponent],
}) })
export class BankAccountsSelectComponent extends AbstractSelectComponent<IBankAccountsResponse> { export class BankAccountsSelectComponent extends AbstractSelectComponent<
IBankAccountsResponse,
IPaginatedResponse<IBankAccountsResponse>
> {
constructor(private service: BankAccountsService) { constructor(private service: BankAccountsService) {
super(); super();
this.getData(); this.getData();
} }
getData() { getDataService() {
this.loading.set(true); return this.service.getAll();
this.service.getAll().subscribe({
next: (res) => {
this.items.set(res.data);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
} }
} }
@@ -1,3 +1,4 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { AbstractSelectComponent } from '@/shared/components'; import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit'; import { UikitFieldComponent } from '@/uikit';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@@ -13,22 +14,16 @@ import { BankBranchFormComponent } from '../form.component';
templateUrl: './select.component.html', templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent, Button, BankBranchFormComponent], imports: [ReactiveFormsModule, Select, UikitFieldComponent, Button, BankBranchFormComponent],
}) })
export class BankBranchesSelectComponent extends AbstractSelectComponent<IBankBranchResponse> { export class BankBranchesSelectComponent extends AbstractSelectComponent<
IBankBranchResponse,
IPaginatedResponse<IBankBranchResponse>
> {
constructor(private service: BankBranchesService) { constructor(private service: BankBranchesService) {
super(); super();
this.getData(); this.getData();
} }
getData() { getDataService() {
this.loading.set(true); return this.service.getAll();
this.service.getAll().subscribe({
next: (res) => {
this.items.set(res.data);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
} }
} }
@@ -1,9 +1,17 @@
<form [formGroup]="form" class="flex items-end gap-4"> <form [formGroup]="form" class="flex items-end gap-4">
<div class="grow grid grid-cols-4 gap-2"> <div class="grow grid grid-cols-4 gap-2">
<inventories-select-field [control]="form.controls.inventoryId" (onChange)="onChangeInventory($event)" /> <inventories-select-field
<products-select-field [control]="form.controls.productId" (onChange)="onChangeProduct($event)" /> [control]="form.controls.inventoryId"
<uikit-datepicker [control]="form.controls.startDate" /> (onChange)="onChangeInventory($event)"
<uikit-datepicker [control]="form.controls.endDate" /> (onSetDefaultDataItem)="setDefaultInventory($event)"
/>
<products-select-field
[control]="form.controls.productId"
(onChange)="onChangeProduct($event)"
(onSetDefaultDataItem)="setDefaultProduct($event)"
/>
<uikit-datepicker [control]="form.controls.startDate" label="از تاریخ" />
<uikit-datepicker [control]="form.controls.endDate" label="تا تاریخ" />
</div> </div>
<div class="shrink-0"> <div class="shrink-0">
<button pButton (click)="search()">جستجو</button> <button pButton (click)="search()">جستجو</button>
@@ -1,6 +1,8 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import { InventoriesSelectComponent } from '@/modules/inventories/components'; import { InventoriesSelectComponent } from '@/modules/inventories/components';
import { IInventorySummaryResponse } from '@/modules/inventories/models';
import { ProductsSelectComponent } from '@/modules/products/components/select/select.component'; import { ProductsSelectComponent } from '@/modules/products/components/select/select.component';
import { IProductResponse } from '@/modules/products/models';
import { UikitFlatpickrJalaliComponent } from '@/uikit'; import { UikitFlatpickrJalaliComponent } from '@/uikit';
import { Component, EventEmitter, inject, Output } from '@angular/core'; import { Component, EventEmitter, inject, Output } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
@@ -34,6 +36,7 @@ export class CardexFiltersComponent {
constructor(private router: Router) { constructor(private router: Router) {
this.filters = this.store.filters(); this.filters = this.store.filters();
this.form.patchValue(this.filters);
} }
form = this.fb.group({ form = this.fb.group({
@@ -55,4 +58,13 @@ export class CardexFiltersComponent {
this.store.getCardex(); this.store.getCardex();
} }
setDefaultInventory(inventory: IInventorySummaryResponse) {
this.store.setDefaultInventory(inventory);
}
setDefaultProduct(product: IProductResponse) {
console.log('setDefaultProduct');
this.store.setDefaultProduct(product);
}
} }
@@ -13,7 +13,11 @@
<th rowspan="2" [style]="{ textAlign: 'center', width: '12rem' }">شرح</th> <th rowspan="2" [style]="{ textAlign: 'center', width: '12rem' }">شرح</th>
<th rowspan="2" [style]="{ textAlign: 'center', width: '6rem' }">شماره</th> <th rowspan="2" [style]="{ textAlign: 'center', width: '6rem' }">شماره</th>
<th rowspan="2" [style]="{ textAlign: 'center', minWidth: '14rem', width: '14rem' }">طرف حساب</th> <th rowspan="2" [style]="{ textAlign: 'center', minWidth: '14rem', width: '14rem' }">طرف حساب</th>
<th rowspan="2" [style]="{ textAlign: 'center', width: '12rem' }">کالا</th> @if (variant === "inventory") {
<th rowspan="2" [style]="{ textAlign: 'center', width: '12rem' }">کالا</th>
} @else if (variant === "product") {
<th rowspan="2" [style]="{ textAlign: 'center', width: '12rem' }">انبار</th>
}
<th colspan="2" class="text-center!" [style]="{ width: '15rem' }">ورودی</th> <th colspan="2" class="text-center!" [style]="{ width: '15rem' }">ورودی</th>
<th colspan="2" class="text-center!" [style]="{ width: '15rem' }">خروجی</th> <th colspan="2" class="text-center!" [style]="{ width: '15rem' }">خروجی</th>
<th colspan="1" class="text-center!" [style]="{ width: '5rem' }">مانده</th> <th colspan="1" class="text-center!" [style]="{ width: '5rem' }">مانده</th>
@@ -44,7 +48,11 @@
<td class="text-center!"> <td class="text-center!">
{{ (item.referenceType === "SALES" ? item.customer?.name : item.supplier?.name) || "-" }} {{ (item.referenceType === "SALES" ? item.customer?.name : item.supplier?.name) || "-" }}
</td> </td>
<td class="text-center!">{{ item.product.name || "-" }}</td> @if (variant === "inventory") {
<td class="text-center!">{{ item.product.name || "-" }}</td>
} @else if (variant === "product") {
<td class="text-center!">{{ item.inventory.name || "-" }}</td>
}
<td class="text-center!"> <td class="text-center!">
{{ item.type === "IN" ? item.quantity : 0 }} {{ item.type === "IN" ? item.quantity : 0 }}
</td> </td>
@@ -18,5 +18,6 @@ import { IInventoryCardex } from './types';
export class CardexComponent { export class CardexComponent {
@Input() items: IInventoryCardex[] = []; @Input() items: IInventoryCardex[] = [];
@Input() loading: boolean = false; @Input() loading: boolean = false;
@Input() variant: 'inventory' | 'product' | 'general' = 'inventory';
constructor() {} constructor() {}
} }
+37 -16
View File
@@ -15,7 +15,7 @@ export interface CardexState extends PaginatedState<ICardexResponse> {
filters: ICardexRequestPayload; filters: ICardexRequestPayload;
selectedInventory: Maybe<ICardexInventorySummary>; selectedInventory: Maybe<ICardexInventorySummary>;
selectedProduct: Maybe<ICardexProductSummary>; selectedProduct: Maybe<ICardexProductSummary>;
variant: 'inventory' | 'product' | 'both'; variant: 'inventory' | 'product' | 'general';
canChangeVariant: boolean; canChangeVariant: boolean;
} }
@@ -28,17 +28,17 @@ export class CardexStore extends PaginatedStore<ICardexResponse, CardexState> {
filters: {}, filters: {},
selectedInventory: null, selectedInventory: null,
selectedProduct: null, selectedProduct: null,
variant: 'both', variant: 'general',
canChangeVariant: true, canChangeVariant: true,
}; };
constructor( constructor(
private router: Router, private router: Router,
private activeRoute: ActivatedRoute, private route: ActivatedRoute,
private service: CardexService, private service: CardexService,
private toastService: ToastService, private toastService: ToastService,
) { ) {
const queryParams = activeRoute.snapshot.queryParams; const queryParams = route.snapshot.queryParams;
super({ super({
loading: false, loading: false,
@@ -52,14 +52,16 @@ export class CardexStore extends PaginatedStore<ICardexResponse, CardexState> {
totalPages: 0, totalPages: 0,
hasMore: false, hasMore: false,
filters: { filters: {
inventoryId: queryParams['inventoryId'], inventoryId: queryParams['inventoryId']
productId: queryParams['productId'], ? parseInt(queryParams['inventoryId'], 10)
: undefined,
productId: queryParams['productId'] ? parseInt(queryParams['productId'], 10) : undefined,
startDate: queryParams['startDate'], startDate: queryParams['startDate'],
endDate: queryParams['endDate'], endDate: queryParams['endDate'],
}, },
selectedInventory: null, selectedInventory: null,
selectedProduct: null, selectedProduct: null,
variant: 'both', variant: 'general',
canChangeVariant: true, canChangeVariant: true,
}); });
this.initial(); this.initial();
@@ -75,7 +77,9 @@ export class CardexStore extends PaginatedStore<ICardexResponse, CardexState> {
case 'inventory': case 'inventory':
return `کاردکس انبار ${this._state().selectedInventory?.name}`; return `کاردکس انبار ${this._state().selectedInventory?.name}`;
default: default:
return 'کاردکس'; return `کاردکس ${this._state().selectedProduct ? 'کالای ' + this._state().selectedProduct?.name : ''} ${
this._state().selectedInventory ? 'در انبار ' + this._state().selectedInventory?.name : ''
}`;
} }
}); });
@@ -83,7 +87,7 @@ export class CardexStore extends PaginatedStore<ICardexResponse, CardexState> {
* Reset state to initial values * Reset state to initial values
*/ */
reset(): void { reset(): void {
const queryParams = this.activeRoute.snapshot.queryParams; const queryParams = this.route.snapshot.queryParams;
this.setState({ this.setState({
loading: false, loading: false,
initialized: false, initialized: false,
@@ -103,7 +107,7 @@ export class CardexStore extends PaginatedStore<ICardexResponse, CardexState> {
}, },
selectedInventory: null, selectedInventory: null,
selectedProduct: null, selectedProduct: null,
variant: 'both', variant: 'general',
canChangeVariant: this.state().canChangeVariant, canChangeVariant: this.state().canChangeVariant,
}); });
} }
@@ -132,7 +136,7 @@ export class CardexStore extends PaginatedStore<ICardexResponse, CardexState> {
const { productId, inventoryId, startDate, endDate } = this.filters(); const { productId, inventoryId, startDate, endDate } = this.filters();
const filters = {} as Partial<ICardexRequestPayload>; const filters = {} as Partial<ICardexRequestPayload>;
Object.entries(this.filters()).forEach(([key, value]) => { Object.entries(this.filters()).forEach(([key, value]) => {
if (value) { if (value && value !== '0') {
filters[key as keyof ICardexRequestPayload] = value; filters[key as keyof ICardexRequestPayload] = value;
} }
}); });
@@ -151,10 +155,19 @@ export class CardexStore extends PaginatedStore<ICardexResponse, CardexState> {
updateFilter(filters: ICardexRequestPayload) { updateFilter(filters: ICardexRequestPayload) {
this.updateVariant(); this.updateVariant();
this.router.navigate([], { console.log('filters');
queryParams: filters, console.log(filters);
skipLocationChange: true, const cleanedFilters = this.validateFilters() || {};
});
this.router
.navigate([], {
relativeTo: this.route,
queryParams: cleanedFilters,
})
.then((res) => console.log(res))
.catch((err) => {
console.log(err);
});
this.patchState({ filters }); this.patchState({ filters });
} }
@@ -169,10 +182,18 @@ export class CardexStore extends PaginatedStore<ICardexResponse, CardexState> {
this.patchState({ canChangeVariant: status }); this.patchState({ canChangeVariant: status });
} }
setDefaultInventory(inventory: ICardexInventorySummary) {
this.patchState({ selectedInventory: inventory });
}
setDefaultProduct(product: ICardexProductSummary) {
this.patchState({ selectedProduct: product });
}
private updateVariant() { private updateVariant() {
const inventoryIsSelected = Boolean(this._state().selectedInventory); const inventoryIsSelected = Boolean(this._state().selectedInventory);
const productIsSelected = Boolean(this._state().selectedProduct); const productIsSelected = Boolean(this._state().selectedProduct);
let variant = 'both' as 'product' | 'inventory' | 'both'; let variant = 'general' as 'product' | 'inventory' | 'general';
if (inventoryIsSelected) { if (inventoryIsSelected) {
if (!productIsSelected) { if (!productIsSelected) {
variant = 'inventory'; variant = 'inventory';
@@ -7,9 +7,8 @@
<uikit-empty-state title="برای مشاهده‌ی کاردکس اطلاعات بالا را تکمیل کنید"> </uikit-empty-state> <uikit-empty-state title="برای مشاهده‌ی کاردکس اطلاعات بالا را تکمیل کنید"> </uikit-empty-state>
} @else { } @else {
<h3 class="mb-4">{{ cardexTitle }}</h3> <h3 class="mb-4">{{ cardexTitle }}</h3>
@if (variant === "inventory") {
<cardex-templates-inventory [items]="items" [loading]="loading" /> <cardex-templates-inventory [items]="items" [loading]="loading" [variant]="variant" />
}
} }
</p-card> </p-card>
</div> </div>
@@ -1,3 +1,4 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { AbstractSelectComponent } from '@/shared/components'; import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit'; import { UikitFieldComponent } from '@/uikit';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@@ -13,22 +14,16 @@ import { CustomerFormComponent } from '../form.component';
templateUrl: './select.component.html', templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent, Button, CustomerFormComponent], imports: [ReactiveFormsModule, Select, UikitFieldComponent, Button, CustomerFormComponent],
}) })
export class CustomersSelectComponent extends AbstractSelectComponent<ICustomerResponse> { export class CustomersSelectComponent extends AbstractSelectComponent<
ICustomerResponse,
IPaginatedResponse<ICustomerResponse>
> {
constructor(private service: CustomersService) { constructor(private service: CustomersService) {
super(); super();
this.getData(); this.getData();
} }
getData() { getDataService() {
this.loading.set(true); return this.service.getAll();
this.service.getAll().subscribe({
next: (res) => {
this.items.set(res.data);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
} }
} }
@@ -1,3 +1,4 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { AbstractSelectComponent } from '@/shared/components'; import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit'; import { UikitFieldComponent } from '@/uikit';
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
@@ -19,27 +20,19 @@ import { InventoryBankAccountFormComponent } from './form.component';
ReactiveFormsModule, ReactiveFormsModule,
], ],
}) })
export class InventoryBankAccountSelectComponent extends AbstractSelectComponent<IInventoryBankAccountResponse> { export class InventoryBankAccountSelectComponent extends AbstractSelectComponent<
IInventoryBankAccountResponse,
IPaginatedResponse<IInventoryBankAccountResponse>
> {
@Input() inventoryId!: string; @Input() inventoryId!: string;
@Input() override showClear: boolean = false; @Input() override showClear: boolean = false;
constructor(private service: InventoryBankAccountsService) { constructor(private service: InventoryBankAccountsService) {
super(); super();
}
ngOnInit() {
this.getData(); this.getData();
} }
getData(): void { getDataService() {
this.loading.set(true); return this.service.getBankAccounts(Number(this.inventoryId));
this.service.getBankAccounts(Number(this.inventoryId)).subscribe({
next: (res) => {
this.loading.set(false);
this.items.set(res.data);
},
error: () => {
this.loading.set(false);
},
});
} }
} }
@@ -1,3 +1,4 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { AbstractSelectComponent } from '@/shared/components'; import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit'; import { UikitFieldComponent } from '@/uikit';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@@ -11,22 +12,16 @@ import { InventoriesService } from '../../services/main.service';
templateUrl: './select.component.html', templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent], imports: [ReactiveFormsModule, Select, UikitFieldComponent],
}) })
export class InventoriesSelectComponent extends AbstractSelectComponent<IInventorySummaryResponse> { export class InventoriesSelectComponent extends AbstractSelectComponent<
IInventorySummaryResponse,
IPaginatedResponse<IInventorySummaryResponse>
> {
constructor(private service: InventoriesService) { constructor(private service: InventoriesService) {
super(); super();
this.getData(); this.getData();
} }
getData() { getDataService() {
this.loading.set(true); return this.service.getAll();
this.service.getAll().subscribe({
next: (res) => {
this.items.set(res.data);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
} }
} }
@@ -1,3 +1,4 @@
import ISummary from '@/core/models/summary';
import { IInventoryBankAccountSummary } from './types'; import { IInventoryBankAccountSummary } from './types';
export interface IInventoryPosAccountRawResponse { export interface IInventoryPosAccountRawResponse {
@@ -6,7 +7,7 @@ export interface IInventoryPosAccountRawResponse {
code: string; code: string;
description: string; description: string;
bankAccountId: number; bankAccountId: number;
inventoryId: number; inventory: ISummary;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
deletedAt: null; deletedAt: null;
@@ -0,0 +1,80 @@
import { ToastService } from '@/core/services/toast.service';
import { EntityState, EntityStore } from '@/core/state';
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { IInventoryDetailResponse } from '../models';
import { InventoriesService } from '../services';
export interface InventoryState extends EntityState<IInventoryDetailResponse> {}
@Injectable({
providedIn: 'root',
})
export class InventoryStore extends EntityStore<IInventoryDetailResponse, InventoryState> {
private readonly _inventoryState = {
isRefreshing: false,
};
constructor(
private activeRoute: ActivatedRoute,
private service: InventoriesService,
private toastService: ToastService,
) {
super({
loading: false,
initialized: false,
error: null,
isRefreshing: false,
entities: {},
selectedId: null,
ids: [],
});
this.initial();
}
supplierId!: string;
initial() {}
getSingle(inventoryId: string) {
if (this.entities()[inventoryId]) {
return;
}
this.patchState({ loading: true });
this.service.getSingle(inventoryId).subscribe({
next: (res) => {
this.patchState({
entities: { [res.id]: res },
loading: false,
});
},
error: (err) => {
this.patchState({ loading: false, error: err });
this.toastService.error({
text: 'خطا در دریافت اطلاعات انبار',
});
},
});
}
refresh(inventoryId: string) {
const { inventoryId: _, ...entities } = this.entities();
this.patchState({
entities,
});
this.getSingle(inventoryId);
}
reset(): void {
const queryParams = this.activeRoute.snapshot.queryParams;
this.setState({
loading: false,
initialized: false,
error: null,
isRefreshing: false,
entities: {},
selectedId: null,
ids: [],
});
}
}
@@ -1,21 +1,22 @@
<p-card class=""> <p-card class="">
<ng-template #header> <ng-template #header>
<div class="p-4 flex items-center justify-between"> <app-inner-pages-header [pageTitle]="pageTitle()" [backRoute]="['/inventories', inventoryId]" class="p-4! block">
<span class="text-xl font-bold">نقاط فروش متصل به انبار</span> <ng-template #actions>
<div class="flex gap-2">
<button pButton outlined (click)="openAddForm()">ایجاد نقطه فروش جدید</button> <button pButton outlined (click)="openAddForm()">ایجاد نقطه فروش جدید</button>
<button pButton outlined icon="pi pi-refresh" [loading]="loading()" (click)="getItems()"></button> <button pButton outlined icon="pi pi-refresh" [loading]="loading()" (click)="getItems()"></button>
</div> </ng-template>
</div> </app-inner-pages-header>
</ng-template> </ng-template>
<app-page-data-list <app-page-data-list
[columns]="columns" [columns]="columns"
emptyPlaceholderTitle="نقطه فروش برای این انبار تعریف نشده است" emptyPlaceholderTitle="نقطه فروش برای این انبار تعریف نشده است"
emptyPlaceholderDescription="برای تعریف نقطه‌ی فروش، روی دکمهٔ بالا کلیک کنید." emptyPlaceholderDescription="برای تعریف نقطه‌ی فروش، روی دکمهٔ بالا کلیک کنید."
[showDetails]="true"
[items]="items()" [items]="items()"
[loading]="loading()" [loading]="loading()"
(onAdd)="openAddForm()" (onAdd)="openAddForm()"
(onDetails)="toPos($event)"
/> />
@if (inventoryId) { @if (inventoryId) {
<app-inventory-pos-account-form [(visible)]="visibleForm" [inventoryId]="inventoryId" /> <app-inventory-pos-account-form [(visible)]="visibleForm" [inventoryId]="inventoryId" />
@@ -1,32 +1,44 @@
import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component';
import { import {
IColumn, IColumn,
PageDataListComponent, PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component'; } from '@/shared/components/pageDataList/page-data-list.component';
import { Component, inject, signal } from '@angular/core'; import { Component, computed, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card'; import { Card } from 'primeng/card';
import { InventoryPosAccountFormComponent } from '../../components/posAccounts/form.component'; import { InventoryPosAccountFormComponent } from '../../components/posAccounts/form.component';
import { IInventoryPosAccountResponse } from '../../models'; import { IInventoryPosAccountResponse } from '../../models';
import { InventoryPosAccountsService } from '../../services/posAccounts.service'; import { InventoryPosAccountsService } from '../../services/posAccounts.service';
import { InventoryStore } from '../../store/inventory.store';
@Component({ @Component({
selector: 'app-inventory-pos-account-list', selector: 'app-inventory-pos-account-list',
templateUrl: './list.component.html', templateUrl: './list.component.html',
imports: [PageDataListComponent, Card, ButtonDirective, InventoryPosAccountFormComponent], imports: [
PageDataListComponent,
Card,
ButtonDirective,
InventoryPosAccountFormComponent,
InnerPagesHeaderComponent,
],
}) })
export class InventoryPosAccountListComponent { export class InventoryPosAccountListComponent {
private readonly route = inject(ActivatedRoute); private readonly route = inject(ActivatedRoute);
private readonly service = inject(InventoryPosAccountsService); private readonly service = inject(InventoryPosAccountsService);
private readonly inventoryStore = inject(InventoryStore);
inventoryId = this.route.snapshot.params['inventoryId'];
items = signal<IInventoryPosAccountResponse[]>([]); items = signal<IInventoryPosAccountResponse[]>([]);
loading = signal<boolean>(false); loading = signal<boolean>(false);
visibleForm = signal<boolean>(false); visibleForm = signal<boolean>(false);
inventoryId = this.route.snapshot.params['inventoryId']; inventory = computed(() => this.inventoryStore.entities()[this.inventoryId]);
columns = [] as IColumn<IInventoryPosAccountResponse>[]; columns = [] as IColumn<IInventoryPosAccountResponse>[];
pageTitle = computed(() => `نقاط فروش متصل به انبار ${this.inventory()?.name || ''}`);
ngOnInit() { ngOnInit() {
this.inventoryStore.getSingle(this.inventoryId);
this.getItems(); this.getItems();
this.columns = [ this.columns = [
{ {
@@ -73,4 +85,8 @@ export class InventoryPosAccountListComponent {
openAddForm() { openAddForm() {
this.visibleForm.set(true); this.visibleForm.set(true);
} }
toPos(item: IInventoryPosAccountResponse) {
window.open(`/pos/${item.id}`, '_blank');
}
} }
@@ -1,3 +1,4 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { AbstractSelectComponent } from '@/shared/components'; import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit'; import { UikitFieldComponent } from '@/uikit';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@@ -13,22 +14,16 @@ import { ProductBrandFormComponent } from '../form.component';
templateUrl: './select.component.html', templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent, Button, ProductBrandFormComponent], imports: [ReactiveFormsModule, Select, UikitFieldComponent, Button, ProductBrandFormComponent],
}) })
export class ProductBrandsSelectComponent extends AbstractSelectComponent<IProductBrandResponse> { export class ProductBrandsSelectComponent extends AbstractSelectComponent<
IProductBrandResponse,
IPaginatedResponse<IProductBrandResponse>
> {
constructor(private service: ProductBrandsService) { constructor(private service: ProductBrandsService) {
super(); super();
this.getData(); this.getData();
} }
getData() { getDataService() {
this.loading.set(true); return this.service.getAll();
this.service.getAll().subscribe({
next: (res) => {
this.items.set(res.data);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
} }
} }
@@ -1,3 +1,4 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { AbstractSelectComponent } from '@/shared/components'; import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit'; import { UikitFieldComponent } from '@/uikit';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@@ -13,22 +14,16 @@ import { ProductCategoryFormComponent } from '../form.component';
templateUrl: './select.component.html', templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent, Button, ProductCategoryFormComponent], imports: [ReactiveFormsModule, Select, UikitFieldComponent, Button, ProductCategoryFormComponent],
}) })
export class ProductCategoriesSelectComponent extends AbstractSelectComponent<IProductCategoryResponse> { export class ProductCategoriesSelectComponent extends AbstractSelectComponent<
IProductCategoryResponse,
IPaginatedResponse<IProductCategoryResponse>
> {
constructor(private service: ProductCategoriesService) { constructor(private service: ProductCategoriesService) {
super(); super();
this.getData(); this.getData();
} }
getData() { getDataService() {
this.loading.set(true); return this.service.getAll();
this.service.getAll().subscribe({
next: (res) => {
this.items.set(res.data);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
} }
} }
@@ -1,3 +1,4 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { AbstractSelectComponent } from '@/shared/components'; import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit'; import { UikitFieldComponent } from '@/uikit';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@@ -12,26 +13,16 @@ import { ProductsService } from '../../services/main.service';
templateUrl: './select.component.html', templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent, CommonModule], imports: [ReactiveFormsModule, Select, UikitFieldComponent, CommonModule],
}) })
export class ProductsSelectComponent extends AbstractSelectComponent<IProductResponse> { export class ProductsSelectComponent extends AbstractSelectComponent<
IProductResponse,
IPaginatedResponse<IProductResponse>
> {
constructor(private service: ProductsService) { constructor(private service: ProductsService) {
super(); super();
this.getData(); this.getData();
} }
getLabel(): string { getDataService() {
return 'کالا'; return this.service.getAll();
}
getData() {
this.loading.set(true);
this.service.getAll().subscribe({
next: (res) => {
this.items.set(res.data);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
} }
} }
@@ -1,7 +1,7 @@
<p-card> <p-card>
<ng-template pTemplate="header"> <ng-template pTemplate="header">
<div class="p-4 flex items-center justify-between"> <div class="p-4 flex items-center justify-between">
<span class="text-xl font-bold">تراکنش‌های تامین‌کننده</span> <span class="text-xl font-bold">دفتر کل تامین‌کننده</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@if (showViewAllCTA) { @if (showViewAllCTA) {
<button pButton outlined [routerLink]="['/suppliers', supplierId, 'ledger']">مشاهده‌ی تمامی تراکنش‌ها</button> <button pButton outlined [routerLink]="['/suppliers', supplierId, 'ledger']">مشاهده‌ی تمامی تراکنش‌ها</button>
@@ -24,8 +24,8 @@
<ng-template pTemplate="body" let-item let-i="rowIndex"> <ng-template pTemplate="body" let-item let-i="rowIndex">
<tr> <tr>
<td>{{ i + 1 }}</td> <td>{{ i + 1 }}</td>
<td><span [appPriceMask]="item.debit"></span></td> <td><span [appPriceMask]="item.debit" [class]="item.debit ? 'text-red-500' : ''"></span></td>
<td><span [appPriceMask]="item.credit"></span></td> <td><span [appPriceMask]="item.credit" [class]="item.credit ? 'text-green-500' : ''"></span></td>
<td><span [appPriceMask]="item.balance"></span></td> <td><span [appPriceMask]="item.balance"></span></td>
<td><span [jalaliDate]="item.createdAt"></span></td> <td><span [jalaliDate]="item.createdAt"></span></td>
</tr> </tr>
@@ -1,3 +1,4 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { AbstractSelectComponent } from '@/shared/components'; import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit'; import { UikitFieldComponent } from '@/uikit';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@@ -11,21 +12,16 @@ import { SuppliersService } from '../../services/main.service';
templateUrl: './select.component.html', templateUrl: './select.component.html',
imports: [ReactiveFormsModule, SelectModule, UikitFieldComponent], imports: [ReactiveFormsModule, SelectModule, UikitFieldComponent],
}) })
export class SuppliersSelectComponent extends AbstractSelectComponent<ISupplierResponse> { export class SuppliersSelectComponent extends AbstractSelectComponent<
ISupplierResponse,
IPaginatedResponse<ISupplierResponse>
> {
constructor(private service: SuppliersService) { constructor(private service: SuppliersService) {
super(); super();
this.getData(); this.getData();
} }
getData() {
this.loading.set(true); getDataService() {
this.service.getAll().subscribe({ return this.service.getAll();
next: (res) => {
this.items.set(res.data);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
} }
} }
@@ -11,22 +11,13 @@ import { BanksStore } from '../../store/main.store';
templateUrl: './select.component.html', templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent], imports: [ReactiveFormsModule, Select, UikitFieldComponent],
}) })
export class BanksSelectComponent extends AbstractSelectComponent<IBankResponse> { export class BanksSelectComponent extends AbstractSelectComponent<IBankResponse, IBankResponse[]> {
constructor(private store: BanksStore) { constructor(private store: BanksStore) {
super(); super();
this.getData(); this.getData();
} }
getData() { getDataService() {
this.loading.set(true); return this.store.getItems();
this.store.getItems().subscribe({
next: (res) => {
this.items.set(res || []);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
} }
} }
@@ -1,6 +1,7 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import { Component, EventEmitter, Input, model, Output, signal } from '@angular/core'; import { Component, EventEmitter, Input, model, Output, signal } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { Observable } from 'rxjs';
export interface ISelectItem { export interface ISelectItem {
id: number; id: number;
@@ -11,7 +12,7 @@ export interface ISelectItem {
selector: 'abstract-select-field', selector: 'abstract-select-field',
template: '', template: '',
}) })
export abstract class AbstractSelectComponent<T = ISelectItem> { export abstract class AbstractSelectComponent<T = ISelectItem, serviceResponse = T[]> {
@Input() control = new FormControl<Maybe<number | T>>(null); @Input() control = new FormControl<Maybe<number | T>>(null);
@Input() canInsert: boolean = false; @Input() canInsert: boolean = false;
@Input() showLabel: boolean = true; @Input() showLabel: boolean = true;
@@ -20,6 +21,7 @@ export abstract class AbstractSelectComponent<T = ISelectItem> {
@Input() isFullDataOptionValue: boolean = false; @Input() isFullDataOptionValue: boolean = false;
@Input() optionValue = 'id'; @Input() optionValue = 'id';
@Output() onChange = new EventEmitter<Maybe<T>>(); @Output() onChange = new EventEmitter<Maybe<T>>();
@Output() onSetDefaultDataItem = new EventEmitter<T>();
loading = signal(false); loading = signal(false);
items = signal<Maybe<T[]>>(null); items = signal<Maybe<T[]>>(null);
@@ -36,7 +38,40 @@ export abstract class AbstractSelectComponent<T = ISelectItem> {
}); });
} }
abstract getData(): void; abstract getDataService(): Observable<serviceResponse>;
getData() {
this.loading.set(true);
this.getDataService().subscribe({
next: (res) => {
if (Array.isArray(res)) {
this.items.set(res);
// @ts-ignore
} else if ('data' in res) {
// @ts-ignore
this.items.set(res.data);
}
const defaultValue = this.control.value;
if (defaultValue) {
const selectedItem = this.items()?.find(
(item) => (item as Record<string, any>)[this.optionValue] == defaultValue,
) as T;
if (selectedItem) {
this.onSetDefaultDataItem.emit(selectedItem);
}
}
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
}
setDefaultDataItem(item: T) {
this.onSetDefaultDataItem.emit(item);
}
refresh() { refresh() {
this.getData(); this.getData();
@@ -54,6 +89,8 @@ export abstract class AbstractSelectComponent<T = ISelectItem> {
} }
change(value: Maybe<T>) { change(value: Maybe<T>) {
console.log('first');
if (typeof value !== 'object') { if (typeof value !== 'object') {
// @ts-ignore // @ts-ignore
this.onChange.emit(this.items()?.find((item) => item[this.optionValue] === value)); this.onChange.emit(this.items()?.find((item) => item[this.optionValue] === value));
@@ -64,4 +64,15 @@
<td class="text-center!"><span [appPriceMask]="item.totalCost"></span></td> --> <td class="text-center!"><span [appPriceMask]="item.totalCost"></span></td> -->
</tr> </tr>
</ng-template> </ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td [attr.colspan]="11">
<uikit-empty-state
[title]="'هیچ رکوردی یافت نشد'"
[description]="'برای این بازه زمانی و فیلترهای انتخاب شده، رکوردی وجود ندارد.'"
></uikit-empty-state>
</td>
</tr>
</ng-template>
</p-table> </p-table>
@@ -1,5 +1,6 @@
import { CatalogMovementReferenceTagComponent } from '@/shared/catalog/movementReferenceTypes'; import { CatalogMovementReferenceTagComponent } from '@/shared/catalog/movementReferenceTypes';
import { JalaliDateDirective, PriceMaskDirective } from '@/shared/directives'; import { JalaliDateDirective, PriceMaskDirective } from '@/shared/directives';
import { UikitEmptyStateComponent } from '@/uikit';
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { TableModule } from 'primeng/table'; import { TableModule } from 'primeng/table';
import { ICardexItem } from './type'; import { ICardexItem } from './type';
@@ -12,6 +13,7 @@ import { ICardexItem } from './type';
JalaliDateDirective, JalaliDateDirective,
CatalogMovementReferenceTagComponent, CatalogMovementReferenceTagComponent,
PriceMaskDirective, PriceMaskDirective,
UikitEmptyStateComponent,
], ],
hostDirectives: [PriceMaskDirective], hostDirectives: [PriceMaskDirective],
}) })
@@ -4,7 +4,7 @@
@if (backRoute) { @if (backRoute) {
<p-button icon="pi pi-arrow-right" aria-label="بازگشت" outlined [routerLink]="backRoute" /> <p-button icon="pi pi-arrow-right" aria-label="بازگشت" outlined [routerLink]="backRoute" />
} }
<h3 class="m-0">{{ pageTitle }}</h3> <h4 class="m-0">{{ pageTitle }}</h4>
</div> </div>
</div> </div>
@if (actions) { @if (actions) {
@@ -1,12 +1,12 @@
<div class="flex flex-col items-center justify-center py-12 text-center bg-surface-overlay"> <div class="block text-center py-12 bg-surface-overlay">
<ng-container> <ng-container>
<i [class]="icon + '!text-6xl mb-4 text-surface-500'" aria-hidden="true"></i> <i [class]="icon + '!text-6xl mb-4 text-surface-500'" aria-hidden="true"></i>
</ng-container> </ng-container>
<span class="text-xl font-semibold mb-2 text-surface-500">{{ title }}</span> <span class="text-xl font-semibold mb-2 text-surface-500 block">{{ title }}</span>
@if (description) { @if (description) {
<p class="text-surface-500 inline-block">{{ description }}</p> <p class="text-surface-500 inline-block">{{ description }}</p>
} }
<ng-content select="[cta]" #cta class="mt-4"></ng-content> <ng-content select="[cta]" #cta class="mt-4 block"></ng-content>
@if (!hasCtaContent && ctaLabel && showCTA) { @if (!hasCtaContent && ctaLabel && showCTA) {
<p-button class="mt-2" size="small" (click)="ctaClick.emit()"> <p-button class="mt-2" size="small" (click)="ctaClick.emit()">
{{ ctaLabel }} {{ ctaLabel }}