feat(cardex): implement cardex module with filters, templates, and service integration
This commit is contained in:
@@ -13,6 +13,7 @@ export interface BaseState<T = any> {
|
|||||||
items?: T;
|
items?: T;
|
||||||
meta?: Maybe<IPaginatedQuery>;
|
meta?: Maybe<IPaginatedQuery>;
|
||||||
isRefreshing?: boolean;
|
isRefreshing?: boolean;
|
||||||
|
initialized: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,6 +78,7 @@ export abstract class BaseStore<T extends BaseState> {
|
|||||||
readonly state: Signal<T>;
|
readonly state: Signal<T>;
|
||||||
readonly loading: Signal<boolean>;
|
readonly loading: Signal<boolean>;
|
||||||
readonly error: Signal<Maybe<string>>;
|
readonly error: Signal<Maybe<string>>;
|
||||||
|
readonly initialized: Signal<boolean>;
|
||||||
|
|
||||||
// Observable for RxJS compatibility
|
// Observable for RxJS compatibility
|
||||||
readonly state$: Observable<T>;
|
readonly state$: Observable<T>;
|
||||||
@@ -89,6 +91,7 @@ export abstract class BaseStore<T extends BaseState> {
|
|||||||
this.state = this._state.asReadonly();
|
this.state = this._state.asReadonly();
|
||||||
this.loading = computed(() => this._state().loading);
|
this.loading = computed(() => this._state().loading);
|
||||||
this.error = computed(() => this._state().error);
|
this.error = computed(() => this._state().error);
|
||||||
|
this.initialized = computed(() => this._state().initialized);
|
||||||
|
|
||||||
// Observable for RxJS compatibility
|
// Observable for RxJS compatibility
|
||||||
this.state$ = this._stateSubject.asObservable();
|
this.state$ = this._stateSubject.asObservable();
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export class GlobalStore extends BaseStore<GlobalState> {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
|
initialized: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
sidebarCollapsed: false,
|
sidebarCollapsed: false,
|
||||||
@@ -300,6 +301,7 @@ export class GlobalStore extends BaseStore<GlobalState> {
|
|||||||
*/
|
*/
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
initialized: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
sidebarCollapsed: false,
|
sidebarCollapsed: false,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export class AuthStore extends BaseStore<AuthState> {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
|
initialized: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
user: null,
|
user: null,
|
||||||
@@ -385,6 +386,7 @@ export class AuthStore extends BaseStore<AuthState> {
|
|||||||
*/
|
*/
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
initialized: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
user: null,
|
user: null,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<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" />
|
<inventories-select-field [control]="form.controls.inventoryId" (onChange)="onChangeInventory($event)" />
|
||||||
<products-select-field [control]="form.controls.productId" />
|
<products-select-field [control]="form.controls.productId" (onChange)="onChangeProduct($event)" />
|
||||||
<uikit-datepicker [control]="form.controls.startDate" />
|
<uikit-datepicker [control]="form.controls.startDate" />
|
||||||
<uikit-datepicker [control]="form.controls.endDate" />
|
<uikit-datepicker [control]="form.controls.endDate" />
|
||||||
</div>
|
</div>
|
||||||
<div class="shrink-0">
|
<div class="shrink-0">
|
||||||
<button pButton>جستجو</button>
|
<button pButton (click)="search()">جستجو</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
|
import { Maybe } from '@/core';
|
||||||
import { InventoriesSelectComponent } from '@/modules/inventories/components';
|
import { InventoriesSelectComponent } from '@/modules/inventories/components';
|
||||||
import { ProductsSelectComponent } from '@/modules/products/components/select/select.component';
|
import { ProductsSelectComponent } from '@/modules/products/components/select/select.component';
|
||||||
import { UikitFlatpickrJalaliComponent } from '@/uikit';
|
import { UikitFlatpickrJalaliComponent } from '@/uikit';
|
||||||
import { Component, inject } from '@angular/core';
|
import { Component, EventEmitter, inject, Output } from '@angular/core';
|
||||||
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { ButtonDirective } from 'primeng/button';
|
import { ButtonDirective } from 'primeng/button';
|
||||||
|
import {
|
||||||
|
ICardexInventorySummary,
|
||||||
|
ICardexProductSummary,
|
||||||
|
ICardexRequestPayload,
|
||||||
|
} from '../../models';
|
||||||
|
import { CardexStore } from '../../store/main.store';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cardex-filters',
|
selector: 'cardex-filters',
|
||||||
@@ -20,12 +27,32 @@ import { ButtonDirective } from 'primeng/button';
|
|||||||
export class CardexFiltersComponent {
|
export class CardexFiltersComponent {
|
||||||
private route = inject(ActivatedRoute);
|
private route = inject(ActivatedRoute);
|
||||||
private fb = inject(FormBuilder);
|
private fb = inject(FormBuilder);
|
||||||
constructor() {}
|
private store = inject(CardexStore);
|
||||||
|
@Output() onSearch = new EventEmitter<void>();
|
||||||
|
|
||||||
|
filters = {} as Partial<ICardexRequestPayload>;
|
||||||
|
|
||||||
|
constructor(private router: Router) {
|
||||||
|
this.filters = this.store.filters();
|
||||||
|
}
|
||||||
|
|
||||||
form = this.fb.group({
|
form = this.fb.group({
|
||||||
inventoryId: [Number(this.route.snapshot.queryParams['inventoryId']) || 0],
|
inventoryId: [this.filters.inventoryId || 0],
|
||||||
productId: [Number(this.route.snapshot.queryParams['productId']) || 0],
|
productId: [this.filters.productId || 0],
|
||||||
startDate: [this.route.snapshot.queryParams['startDate'] || ''],
|
startDate: [this.filters.startDate || ''],
|
||||||
endDate: [this.route.snapshot.queryParams['endDate'] || ''],
|
endDate: [this.filters.endDate || ''],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onChangeInventory(inventory: Maybe<ICardexInventorySummary>) {
|
||||||
|
this.store.setInventory(inventory);
|
||||||
|
}
|
||||||
|
onChangeProduct(product: Maybe<ICardexProductSummary>) {
|
||||||
|
this.store.setProduct(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
search() {
|
||||||
|
this.store.updateFilter(this.form.value as ICardexRequestPayload);
|
||||||
|
|
||||||
|
this.store.getCardex();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<p-table
|
||||||
|
stripedRows
|
||||||
|
[value]="items"
|
||||||
|
[loading]="loading"
|
||||||
|
showGridlines
|
||||||
|
tableStyleClass="table-fixed border-collapse"
|
||||||
|
scrollable
|
||||||
|
>
|
||||||
|
<ng-template #header>
|
||||||
|
<tr>
|
||||||
|
<th rowspan="2" [style]="{ textAlign: 'center', width: '4rem' }">ردیف</th>
|
||||||
|
<th rowspan="2" [style]="{ textAlign: 'center', width: '8rem' }">تاریخ</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', minWidth: '14rem', width: '14rem' }">طرف حساب</th>
|
||||||
|
<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="1" class="text-center!" [style]="{ width: '5rem' }">مانده</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th [style]="{ textAlign: 'center', width: '5rem' }">تعداد</th>
|
||||||
|
<th [style]="{ textAlign: 'center', width: '10rem' }">فی</th>
|
||||||
|
<th [style]="{ textAlign: 'center', width: '5rem' }">تعداد</th>
|
||||||
|
<th [style]="{ textAlign: 'center', width: '10rem' }">فی</th>
|
||||||
|
<th [style]="{ textAlign: 'center', width: '5rem' }">تعداد</th>
|
||||||
|
<!-- <th [style]="{ textAlign: 'center', width: '10rem' }">فی</th>
|
||||||
|
<th [style]="{ textAlign: 'center', width: '10rem' }">مجموع قیمت</th> -->
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #body let-item let-i="rowIndex">
|
||||||
|
<tr>
|
||||||
|
<td class="text-center!">{{ i + 1 }}</td>
|
||||||
|
<td class="text-center!" [jalaliDate]="item.createdAt"></td>
|
||||||
|
<td class="text-center!">
|
||||||
|
<catalog-movement-reference-tag
|
||||||
|
[type]="item.referenceType"
|
||||||
|
[movementType]="item.type"
|
||||||
|
[counterName]="item.counterInventory?.name"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="text-center!">{{ item.referenceId || "-" }}</td>
|
||||||
|
<td class="text-center!">
|
||||||
|
{{ (item.referenceType === "SALES" ? item.customer?.name : item.supplier?.name) || "-" }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center!">{{ item.product.name || "-" }}</td>
|
||||||
|
<td class="text-center!">
|
||||||
|
{{ item.type === "IN" ? item.quantity : 0 }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center!">
|
||||||
|
@if (item.type === "IN") {
|
||||||
|
<span [appPriceMask]="item.fee"></span>
|
||||||
|
} @else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="text-center!">
|
||||||
|
{{ item.type === "OUT" ? item.quantity : 0 }}
|
||||||
|
</td>
|
||||||
|
<td class="text-center!">
|
||||||
|
@if (item.type === "OUT") {
|
||||||
|
<span [appPriceMask]="item.fee"></span>
|
||||||
|
} @else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="text-center!">{{ item.remainedInStock }}</td>
|
||||||
|
<!-- <td class="text-center!"><span [appPriceMask]="item.fee"></span></td>
|
||||||
|
<td class="text-center!"><span [appPriceMask]="item.totalCost"></span></td> -->
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</p-table>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { CatalogMovementReferenceTagComponent } from '@/shared/catalog/movementReferenceTypes';
|
||||||
|
import { JalaliDateDirective, PriceMaskDirective } from '@/shared/directives';
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { TableModule } from 'primeng/table';
|
||||||
|
import { IInventoryCardex } from './types';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'cardex-templates-inventory',
|
||||||
|
templateUrl: './inventory.component.html',
|
||||||
|
imports: [
|
||||||
|
TableModule,
|
||||||
|
JalaliDateDirective,
|
||||||
|
CatalogMovementReferenceTagComponent,
|
||||||
|
PriceMaskDirective,
|
||||||
|
],
|
||||||
|
hostDirectives: [PriceMaskDirective],
|
||||||
|
})
|
||||||
|
export class CardexComponent {
|
||||||
|
@Input() items: IInventoryCardex[] = [];
|
||||||
|
@Input() loading: boolean = false;
|
||||||
|
constructor() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { ICardexResponse } from '../../models';
|
||||||
|
|
||||||
|
export interface IInventoryCardex extends ICardexResponse {}
|
||||||
+37
-59
@@ -1,66 +1,44 @@
|
|||||||
import {
|
import { ICardexInventorySummary, ICardexProductSummary } from './types';
|
||||||
IInventoryInfo,
|
|
||||||
IInventoryMovement,
|
|
||||||
IInventoryStockProduct,
|
|
||||||
IInventoryTransferRequestItem,
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
export interface IInventorySummaryRawResponse {
|
export interface ICardexRawResponse {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
quantity: number;
|
||||||
|
fee: number;
|
||||||
|
totalCost: number;
|
||||||
|
referenceType: string;
|
||||||
|
referenceId: string;
|
||||||
|
createdAt: string;
|
||||||
|
avgCost: number;
|
||||||
|
product: ICardexProductSummary;
|
||||||
|
inventory: ICardexInventorySummary;
|
||||||
|
supplier?: {
|
||||||
|
id: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
};
|
||||||
|
customer?: {
|
||||||
|
id: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
};
|
||||||
|
counterInventory?: ICardexInventorySummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICardexResponse extends ICardexRawResponse {
|
||||||
|
supplier?: {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
location: string;
|
};
|
||||||
isActive: boolean;
|
customer?: {
|
||||||
isPointOfSale: boolean;
|
id: number;
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
deletedAt: Maybe<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IInventorySummaryResponse extends IInventorySummaryRawResponse {}
|
|
||||||
|
|
||||||
export interface IInventoryDetailRawResponse extends IInventorySummaryRawResponse {
|
|
||||||
availableProductTypes: number;
|
|
||||||
availableProductCount: number;
|
|
||||||
availableProductsCost: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IInventoryDetailResponse extends IInventoryDetailRawResponse {}
|
|
||||||
|
|
||||||
export interface IInventoryRequest {
|
|
||||||
name: string;
|
name: string;
|
||||||
location: string;
|
};
|
||||||
isActive: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IInventoryStockMovementRawResponse {
|
export interface ICardexRequestPayload {
|
||||||
receiptId: string;
|
startDate?: string;
|
||||||
count: number;
|
endDate?: string;
|
||||||
info: IInventoryInfo;
|
inventoryId?: number;
|
||||||
movements: IInventoryMovement[];
|
productId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IInventoryStockMovementResponse extends IInventoryStockMovementRawResponse {}
|
|
||||||
|
|
||||||
export interface IInventoryTransferRequest {
|
|
||||||
code: string;
|
|
||||||
description: string;
|
|
||||||
fromInventoryId: number;
|
|
||||||
toInventoryId: number;
|
|
||||||
items: IInventoryTransferRequestItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IInventoryStockRawResponse {
|
|
||||||
quantity: number;
|
|
||||||
avgCost: number;
|
|
||||||
product: IInventoryStockProduct;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IInventoryStockResponse extends IInventoryStockRawResponse {}
|
|
||||||
|
|
||||||
export interface IInventoryStockQuery {
|
|
||||||
isAvailable?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IInventoryProductCardexRawResponse extends IInventoryStockMovementRawResponse {}
|
|
||||||
|
|
||||||
export interface IInventoryProductCardexResponse extends IInventoryProductCardexRawResponse {}
|
|
||||||
|
|||||||
@@ -1,42 +1,10 @@
|
|||||||
import { IProductRawResponse } from '@/modules/products/models';
|
export interface ICardexInventorySummary {
|
||||||
|
name: string;
|
||||||
export interface IInventoryMovement {
|
id: number;
|
||||||
id: number;
|
}
|
||||||
quantity: number;
|
|
||||||
fee: number;
|
export interface ICardexProductSummary {
|
||||||
totalCost: number;
|
|
||||||
avgCost: number;
|
|
||||||
product: IInventoryProduct;
|
|
||||||
remainedInStock: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IInventoryProduct {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
name: string;
|
||||||
description: null;
|
|
||||||
sku: string;
|
sku: string;
|
||||||
barcode: string;
|
id: number;
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
deletedAt: null;
|
|
||||||
brandId: number;
|
|
||||||
categoryId: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IInventoryInfo {
|
|
||||||
date: string;
|
|
||||||
type: string;
|
|
||||||
quantity: number;
|
|
||||||
fee: number;
|
|
||||||
totalCost: number;
|
|
||||||
referenceType: string;
|
|
||||||
referenceId: string;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IInventoryTransferRequestItem {
|
|
||||||
productId: number;
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IInventoryStockProduct extends IProductRawResponse {}
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { IPaginatedResponse } from '@/core/models/service.model';
|
||||||
|
import { getFullName } from '@/utils';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { map, Observable } from 'rxjs';
|
||||||
|
import { CARDEX_API_ROUTES } from '../constants';
|
||||||
|
import { ICardexRawResponse, ICardexRequestPayload, ICardexResponse } from '../models';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CardexService {
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
private apiRoutes = CARDEX_API_ROUTES;
|
||||||
|
|
||||||
|
getCardex(params: ICardexRequestPayload): Observable<IPaginatedResponse<ICardexResponse>> {
|
||||||
|
return this.http
|
||||||
|
.get<IPaginatedResponse<ICardexRawResponse>>(this.apiRoutes.cardex(), {
|
||||||
|
params: { ...params },
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
map((res) => {
|
||||||
|
return {
|
||||||
|
meta: res.meta,
|
||||||
|
data: res.data.map((item) => ({
|
||||||
|
...item,
|
||||||
|
supplier: item.supplier
|
||||||
|
? {
|
||||||
|
...item.supplier,
|
||||||
|
name: getFullName(item.supplier),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
customer: item.customer
|
||||||
|
? {
|
||||||
|
...item.customer,
|
||||||
|
name: getFullName(item.customer),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import { Maybe } from '@/core';
|
||||||
|
import { ToastService } from '@/core/services/toast.service';
|
||||||
|
import { PaginatedState, PaginatedStore } from '@/core/state';
|
||||||
|
import { computed, Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import {
|
||||||
|
ICardexInventorySummary,
|
||||||
|
ICardexProductSummary,
|
||||||
|
ICardexRequestPayload,
|
||||||
|
ICardexResponse,
|
||||||
|
} from '../models';
|
||||||
|
import { CardexService } from '../services/main.service';
|
||||||
|
|
||||||
|
export interface CardexState extends PaginatedState<ICardexResponse> {
|
||||||
|
filters: ICardexRequestPayload;
|
||||||
|
selectedInventory: Maybe<ICardexInventorySummary>;
|
||||||
|
selectedProduct: Maybe<ICardexProductSummary>;
|
||||||
|
variant: 'inventory' | 'product' | 'both';
|
||||||
|
canChangeVariant: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class CardexStore extends PaginatedStore<ICardexResponse, CardexState> {
|
||||||
|
private readonly _cardexState = {
|
||||||
|
isRefreshing: false,
|
||||||
|
filters: {},
|
||||||
|
selectedInventory: null,
|
||||||
|
selectedProduct: null,
|
||||||
|
variant: 'both',
|
||||||
|
canChangeVariant: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private activeRoute: ActivatedRoute,
|
||||||
|
private service: CardexService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
) {
|
||||||
|
const queryParams = activeRoute.snapshot.queryParams;
|
||||||
|
|
||||||
|
super({
|
||||||
|
loading: false,
|
||||||
|
initialized: false,
|
||||||
|
error: null,
|
||||||
|
isRefreshing: false,
|
||||||
|
items: [],
|
||||||
|
totalCount: 0,
|
||||||
|
currentPage: Number(queryParams['page']) || 1,
|
||||||
|
pageSize: Number(queryParams['pageSize']) || 30,
|
||||||
|
totalPages: 0,
|
||||||
|
hasMore: false,
|
||||||
|
filters: {
|
||||||
|
inventoryId: queryParams['inventoryId'],
|
||||||
|
productId: queryParams['productId'],
|
||||||
|
startDate: queryParams['startDate'],
|
||||||
|
endDate: queryParams['endDate'],
|
||||||
|
},
|
||||||
|
selectedInventory: null,
|
||||||
|
selectedProduct: null,
|
||||||
|
variant: 'both',
|
||||||
|
canChangeVariant: true,
|
||||||
|
});
|
||||||
|
this.initial();
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly filters = computed(() => this._state().filters);
|
||||||
|
readonly variant = computed(() => this._state().variant);
|
||||||
|
readonly canChangeVariant = computed(() => this._state().canChangeVariant);
|
||||||
|
readonly cardexTitle = computed(() => {
|
||||||
|
switch (this._state().variant) {
|
||||||
|
case 'product':
|
||||||
|
return `کاردکس کالای ${this._state().selectedProduct?.name}`;
|
||||||
|
case 'inventory':
|
||||||
|
return `کاردکس انبار ${this._state().selectedInventory?.name}`;
|
||||||
|
default:
|
||||||
|
return 'کاردکس';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset state to initial values
|
||||||
|
*/
|
||||||
|
reset(): void {
|
||||||
|
const queryParams = this.activeRoute.snapshot.queryParams;
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
initialized: false,
|
||||||
|
error: null,
|
||||||
|
isRefreshing: false,
|
||||||
|
items: [],
|
||||||
|
totalCount: 0,
|
||||||
|
currentPage: Number(queryParams['page']) || 1,
|
||||||
|
pageSize: Number(queryParams['pageSize']) || 30,
|
||||||
|
totalPages: 0,
|
||||||
|
hasMore: false,
|
||||||
|
filters: {
|
||||||
|
inventoryId: queryParams['inventoryId'],
|
||||||
|
productId: queryParams['productId'],
|
||||||
|
startDate: queryParams['startDate'],
|
||||||
|
endDate: queryParams['endDate'],
|
||||||
|
},
|
||||||
|
selectedInventory: null,
|
||||||
|
selectedProduct: null,
|
||||||
|
variant: 'both',
|
||||||
|
canChangeVariant: this.state().canChangeVariant,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initial() {
|
||||||
|
this.getCardex();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCardex() {
|
||||||
|
const filters = this.validateFilters();
|
||||||
|
if (filters) {
|
||||||
|
this.patchState({ initialized: true });
|
||||||
|
this.setLoading(true);
|
||||||
|
this.service.getCardex(filters).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.setItems(res.data, res.meta);
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.setError(err);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateFilters() {
|
||||||
|
const { productId, inventoryId, startDate, endDate } = this.filters();
|
||||||
|
const filters = {} as Partial<ICardexRequestPayload>;
|
||||||
|
Object.entries(this.filters()).forEach(([key, value]) => {
|
||||||
|
if (value) {
|
||||||
|
filters[key as keyof ICardexRequestPayload] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!productId && !inventoryId) {
|
||||||
|
this.toastService.warn({ text: 'برای مشاهدهی کاردکس حداقل انبار و یا کالا را مشخص کنید' });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageChange(page: number) {
|
||||||
|
this.setCurrentPage(page);
|
||||||
|
this.getCardex();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFilter(filters: ICardexRequestPayload) {
|
||||||
|
this.updateVariant();
|
||||||
|
console.log(filters);
|
||||||
|
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: filters,
|
||||||
|
skipLocationChange: true,
|
||||||
|
});
|
||||||
|
this.patchState({ filters });
|
||||||
|
}
|
||||||
|
|
||||||
|
setProduct(product: Maybe<ICardexProductSummary>) {
|
||||||
|
this.patchState({ selectedProduct: product });
|
||||||
|
}
|
||||||
|
setInventory(inventory: Maybe<ICardexInventorySummary>) {
|
||||||
|
this.patchState({ selectedInventory: inventory });
|
||||||
|
}
|
||||||
|
|
||||||
|
setCanChangeVariant(status: boolean) {
|
||||||
|
this.patchState({ canChangeVariant: status });
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateVariant() {
|
||||||
|
const inventoryIsSelected = Boolean(this._state().selectedInventory);
|
||||||
|
const productIsSelected = Boolean(this._state().selectedProduct);
|
||||||
|
let variant = 'both' as 'product' | 'inventory' | 'both';
|
||||||
|
if (inventoryIsSelected) {
|
||||||
|
if (!productIsSelected) {
|
||||||
|
variant = 'inventory';
|
||||||
|
}
|
||||||
|
} else if (productIsSelected) {
|
||||||
|
variant = 'product';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.patchState({ variant });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,13 @@
|
|||||||
<cardex-filters />
|
<cardex-filters />
|
||||||
</p-card>
|
</p-card>
|
||||||
<p-card>
|
<p-card>
|
||||||
|
@if (!initialized) {
|
||||||
<uikit-empty-state title="برای مشاهدهی کاردکس اطلاعات بالا را تکمیل کنید"> </uikit-empty-state>
|
<uikit-empty-state title="برای مشاهدهی کاردکس اطلاعات بالا را تکمیل کنید"> </uikit-empty-state>
|
||||||
|
} @else {
|
||||||
|
<h3 class="mb-4">{{ cardexTitle }}</h3>
|
||||||
|
@if (variant === "inventory") {
|
||||||
|
<cardex-templates-inventory [items]="items" [loading]="loading" />
|
||||||
|
}
|
||||||
|
}
|
||||||
</p-card>
|
</p-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,12 +2,30 @@ import { UikitEmptyStateComponent } from '@/uikit';
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Card } from 'primeng/card';
|
import { Card } from 'primeng/card';
|
||||||
import { CardexFiltersComponent } from '../components';
|
import { CardexFiltersComponent } from '../components';
|
||||||
|
import { CardexComponent } from '../components/templates/inventory.component';
|
||||||
|
import { CardexStore } from '../store/main.store';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-cardex',
|
selector: 'app-cardex',
|
||||||
templateUrl: './cardex.component.html',
|
templateUrl: './cardex.component.html',
|
||||||
imports: [Card, CardexFiltersComponent, UikitEmptyStateComponent],
|
imports: [Card, CardexFiltersComponent, UikitEmptyStateComponent, CardexComponent],
|
||||||
})
|
})
|
||||||
export class CardexPageComponent {
|
export class CardexPageComponent {
|
||||||
constructor() {}
|
constructor(private store: CardexStore) {}
|
||||||
|
|
||||||
|
get cardexTitle() {
|
||||||
|
return this.store.cardexTitle();
|
||||||
|
}
|
||||||
|
get initialized() {
|
||||||
|
return this.store.initialized();
|
||||||
|
}
|
||||||
|
get loading() {
|
||||||
|
return this.store.loading();
|
||||||
|
}
|
||||||
|
get variant() {
|
||||||
|
return this.store.variant();
|
||||||
|
}
|
||||||
|
get items() {
|
||||||
|
return this.store.items();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
[showClear]="true"
|
[showClear]="true"
|
||||||
[filter]="true"
|
[filter]="true"
|
||||||
appendTo="body"
|
appendTo="body"
|
||||||
|
(onChange)="change($event.value)"
|
||||||
>
|
>
|
||||||
</p-select>
|
</p-select>
|
||||||
</uikit-field>
|
</uikit-field>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
[showClear]="true"
|
[showClear]="true"
|
||||||
[filter]="true"
|
[filter]="true"
|
||||||
appendTo="body"
|
appendTo="body"
|
||||||
|
(onChange)="change($event.value)"
|
||||||
>
|
>
|
||||||
</p-select>
|
</p-select>
|
||||||
</uikit-field>
|
</uikit-field>
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ export interface IProductRawResponse {
|
|||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
barcode?: Maybe<string>;
|
barcode?: Maybe<string>;
|
||||||
sku?: Maybe<string>;
|
sku: string;
|
||||||
// productType: string;
|
// productType: string;
|
||||||
brand?: IProductBrand;
|
brand?: IProductBrand;
|
||||||
category?: IProductCategory;
|
category?: IProductCategory;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export class ProductsStore extends PaginatedStore<IProductResponse> {
|
|||||||
const queryParams = activeRoute.snapshot.queryParams;
|
const queryParams = activeRoute.snapshot.queryParams;
|
||||||
|
|
||||||
super({
|
super({
|
||||||
|
initialized: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
isRefreshing: false,
|
isRefreshing: false,
|
||||||
@@ -40,6 +41,7 @@ export class ProductsStore extends PaginatedStore<IProductResponse> {
|
|||||||
*/
|
*/
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
initialized: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
isRefreshing: false,
|
isRefreshing: false,
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import { getMovementReferenceTypeColor, getMovementReferenceTypeText } from './u
|
|||||||
export class CatalogMovementReferenceTagComponent {
|
export class CatalogMovementReferenceTagComponent {
|
||||||
@Input() type!: MovementReferenceType;
|
@Input() type!: MovementReferenceType;
|
||||||
@Input() movementType?: MovementType;
|
@Input() movementType?: MovementType;
|
||||||
|
@Input() counterName?: string;
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
get text() {
|
get text() {
|
||||||
return getMovementReferenceTypeText(this.type, this.movementType);
|
return getMovementReferenceTypeText(this.type, this.movementType, this.counterName);
|
||||||
}
|
}
|
||||||
get color() {
|
get color() {
|
||||||
return getMovementReferenceTypeColor(this.type);
|
return getMovementReferenceTypeColor(this.type);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { MovementReferenceType } from './types';
|
|||||||
export const getMovementReferenceTypeText = (
|
export const getMovementReferenceTypeText = (
|
||||||
movementType: MovementReferenceType,
|
movementType: MovementReferenceType,
|
||||||
type?: MovementType,
|
type?: MovementType,
|
||||||
|
counterName?: string,
|
||||||
): string => {
|
): string => {
|
||||||
switch (movementType) {
|
switch (movementType) {
|
||||||
case MovementReferenceType.PURCHASE:
|
case MovementReferenceType.PURCHASE:
|
||||||
@@ -15,9 +16,9 @@ export const getMovementReferenceTypeText = (
|
|||||||
return 'تعدیل';
|
return 'تعدیل';
|
||||||
case MovementReferenceType.INVENTORY_TRANSFER:
|
case MovementReferenceType.INVENTORY_TRANSFER:
|
||||||
if (type === MovementType.IN) {
|
if (type === MovementType.IN) {
|
||||||
return 'دریافت از انبار دیگر';
|
return `دریافت از انبار ${counterName ?? 'دیگر'}`;
|
||||||
} else {
|
} else {
|
||||||
return 'ارسال به انبار دیگر';
|
return `ارسال به انبار ${counterName ?? 'دیگر'}`;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return 'انتقال بین انبارها';
|
return 'انتقال بین انبارها';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Maybe } from '@/core';
|
import { Maybe } from '@/core';
|
||||||
import { Component, Input, model, signal } from '@angular/core';
|
import { Component, EventEmitter, Input, model, Output, signal } from '@angular/core';
|
||||||
import { FormControl } from '@angular/forms';
|
import { FormControl } from '@angular/forms';
|
||||||
|
|
||||||
export interface ISelectItem {
|
export interface ISelectItem {
|
||||||
@@ -18,6 +18,7 @@ export abstract class AbstractSelectComponent<T = ISelectItem> {
|
|||||||
@Input() showErrors: boolean = true;
|
@Input() showErrors: boolean = true;
|
||||||
@Input() isFullDataOptionValue: boolean = false;
|
@Input() isFullDataOptionValue: boolean = false;
|
||||||
@Input() optionValue = 'id';
|
@Input() optionValue = 'id';
|
||||||
|
@Output() onChange = new EventEmitter<Maybe<T>>();
|
||||||
|
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
items = signal<Maybe<T[]>>(null);
|
items = signal<Maybe<T[]>>(null);
|
||||||
@@ -50,4 +51,13 @@ export abstract class AbstractSelectComponent<T = ISelectItem> {
|
|||||||
}
|
}
|
||||||
return this.optionValue;
|
return this.optionValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
change(value: Maybe<T>) {
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
// @ts-ignore
|
||||||
|
this.onChange.emit(this.items()?.find((item) => item[this.optionValue] === value));
|
||||||
|
} else {
|
||||||
|
this.onChange.emit(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,22 +5,10 @@
|
|||||||
[showErrors]="showErrors"
|
[showErrors]="showErrors"
|
||||||
class="w-full datepicker"
|
class="w-full datepicker"
|
||||||
>
|
>
|
||||||
|
<div class="w-full">
|
||||||
<div #wrapper [attr.data-disable]="disabled" [attr.data-name]="name" class="w-full">
|
<div #wrapper [attr.data-disable]="disabled" [attr.data-name]="name" class="w-full">
|
||||||
<input pInputText data-input [attr.placeholder]="placeholder" [name]="name" class="w-full" />
|
<input pInputText data-input [attr.placeholder]="placeholder" [name]="name" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flat"></div>
|
<div class="flat"></div>
|
||||||
|
</div>
|
||||||
</uikit-field>
|
</uikit-field>
|
||||||
|
|
||||||
<!--
|
|
||||||
|
|
||||||
<div #wrapper class="flex items-center gap-2" data-enable-time="false" data-click-opens="true">
|
|
||||||
<input pInputText class="w-full bg-white text-sm rounded border p-2" data-input [attr.placeholder]="placeholder" />
|
|
||||||
|
|
||||||
<button pButton type="button" class="p-button-text text-gray-500" data-toggle aria-label="toggle">
|
|
||||||
<i class="pi pi-calendar"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button pButton type="button" class="p-button-text text-gray-500" data-clear aria-label="clear">
|
|
||||||
<i class="pi pi-times"></i>
|
|
||||||
</button>
|
|
||||||
</div> -->
|
|
||||||
|
|||||||
@@ -41,11 +41,13 @@ export class UikitFlatpickrJalaliComponent implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.fpInstance = flatpickr(this.wrapperRef.nativeElement, {
|
this.fpInstance = flatpickr(this.wrapperRef.nativeElement, {
|
||||||
...this.options,
|
...this.options,
|
||||||
now: dayjs().calendar('jalali').format('YYYY/MM/DD'),
|
locale: 'fa',
|
||||||
altInputClass: 'p-inputtext w-full',
|
altInputClass: 'p-inputtext w-full',
|
||||||
|
dateFormat: 'Y-m-d',
|
||||||
|
altFormat: 'Y-m-d',
|
||||||
onChange: (selectedDates: Date[], dateStr: string) => {
|
onChange: (selectedDates: Date[], dateStr: string) => {
|
||||||
this.valueChange.emit(dateStr);
|
this.valueChange.emit(dateStr);
|
||||||
if (this.control) this.control.setValue(dateStr);
|
if (this.control) this.control.setValue(dayjs(dateStr).toISOString());
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user