feat(inventories): implement inventory movement list and transfer functionality

- Added InventoryMovementListComponent to display stock movements with filtering options.
- Created InventoriesTransferFormComponent for transferring stock between inventories.
- Implemented product cardex view to show detailed inventory movements for specific products.
- Enhanced product listing with loading states and improved UI components.
- Introduced Jalali date formatting directive for better date representation.
- Added utility functions for Jalali date conversions and comparisons.
- Created reusable details info card component for displaying inventory details.
- Updated movement reference types and tags for better categorization of inventory movements.
This commit is contained in:
2025-12-14 10:16:14 +03:30
parent b46b8b83f9
commit 35be7e0298
65 changed files with 1543 additions and 80 deletions
@@ -4,7 +4,7 @@ import { FormFooterActionsComponent } from '@/shared/components/formFooterAction
import { Component, effect, EventEmitter, inject, Input, Output, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { Dialog } from 'primeng/dialog';
import { IInventoryRequest, IInventoryResponse } from '../models';
import { IInventoryRequest, IInventorySummaryResponse } from '../models';
import { InventoriesService } from '../services/main.service';
@Component({
@@ -13,7 +13,7 @@ import { InventoriesService } from '../services/main.service';
imports: [ReactiveFormsModule, Dialog, InputComponent, FormFooterActionsComponent],
})
export class InventoryFormComponent {
@Input() initialValues?: IInventoryResponse;
@Input() initialValues?: IInventorySummaryResponse;
@Input()
set visible(v: boolean) {
@@ -25,7 +25,7 @@ export class InventoryFormComponent {
private visibleSignal = signal(false);
@Output() visibleChange = new EventEmitter<boolean>();
@Output() onSubmit = new EventEmitter<IInventoryResponse>();
@Output() onSubmit = new EventEmitter<IInventorySummaryResponse>();
private fb = inject(FormBuilder);
constructor(
@@ -0,0 +1,4 @@
export * from './form.component';
export * from './movementList/movement-list.component';
export * from './select/select.component';
export * from './transfer/transfer-form.component';
@@ -0,0 +1,114 @@
<p-card>
<ng-template pTemplate="header">
<div class="p-4 flex items-center justify-between">
<span class="text-xl font-bold">{{ title }}</span>
<button pButton outlined icon="pi pi-refresh" [loading]="loading()" (click)="getAll()"></button>
</div>
</ng-template>
<ng-template pTemplate="content">
<p-table [value]="movements()" [loading]="loading()" dataKey="receiptId" [expandedRowKeys]="expandedRows">
<ng-template pTemplate="header">
<tr>
<th [style]="{ width: '5rem' }"></th>
<th>شناسه رسید</th>
<th>نوع گردش</th>
<th>انواع کالاها</th>
<th>تعداد کالاها</th>
<th>مجموع قیمت</th>
<th>تاریخ</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-movement let-expanded="expanded">
<tr>
<td>
<p-button
type="button"
pRipple
[pRowToggler]="movement"
text
severity="secondary"
rounded
[icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-left'"
/>
</td>
<td>{{ movement.receiptId }}</td>
<td><catalog-movement-reference-tag [type]="movement.info.referenceType" /></td>
<td>{{ movement.count }}</td>
<td>{{ movement.info.quantity }}</td>
<td><span [appPriceMask]="movement.info.totalCost"></span></td>
<td><span [jalaliDate]="movement.info.createdAt"></span></td>
</tr>
</ng-template>
<ng-template #expandedrow let-movement>
<tr>
<td colspan="7">
<p-card class="p-4 pt-0 border border-primary-700">
<p-table [value]="movement.movements" dataKey="receiptId">
<ng-template #caption>
<h5 class="mb-4">کالاهای جابجا شده در این گردش</h5>
</ng-template>
<ng-template #header>
<tr>
<th pSortableColumn="product.id">
<div class="flex items-center gap-2">
شناسه کالا
<p-sortIcon field="product.id" />
</div>
</th>
<th pSortableColumn="product.name">
<div class="flex items-center gap-2">
عنوان کالا
<p-sortIcon field="product.name" />
</div>
</th>
<th pSortableColumn="quantity">
<div class="flex items-center gap-2">
تعداد
<p-sortIcon field="quantity" />
</div>
</th>
<th pSortableColumn="fee">
<div class="flex items-center gap-2">
قیمت واحد
<p-sortIcon field="fee" />
</div>
</th>
<th pSortableColumn="totalCost">
<div class="flex items-center gap-2">
قیمت نهایی
<p-sortIcon field="totalCost" />
</div>
</th>
<th style="width: 4rem"></th>
</tr>
</ng-template>
<ng-template #body let-movement>
<tr>
<td>{{ movement.product.id }}</td>
<td>{{ movement.product.name }}</td>
<td>{{ movement.quantity }}</td>
<td><span [appPriceMask]="movement.fee"></span></td>
<td><span [appPriceMask]="movement.totalCost"></span></td>
<td>
<!-- <p-tag [value]="order.status" [severity]="getStatusSeverity(order.status)" /> -->
</td>
<td>
<p-button type="button" icon="pi pi-search" [routerLink]="'/products/' + movement.product.id" />
</td>
</tr>
</ng-template>
</p-table>
</p-card>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="7" class="text-center! p-10!">هیچ گردش موجودی یافت نشد.</td>
</tr>
</ng-template>
</p-table>
</ng-template>
</p-card>
@@ -0,0 +1,69 @@
import { MovementType } from '@/shared/catalog';
import { CatalogMovementReferenceTagComponent } from '@/shared/catalog/movementReferenceTypes';
import { JalaliDateDirective, PriceMaskDirective } from '@/shared/directives';
import { Component, Input, signal } from '@angular/core';
import { RouterLink } from '@angular/router';
import { Button, ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card';
import { Ripple } from 'primeng/ripple';
import { TableModule } from 'primeng/table';
import { IInventoryStockMovementResponse } from '../../models';
import { InventoriesService } from '../../services/main.service';
@Component({
selector: 'inventory-movement-list',
templateUrl: './movement-list.component.html',
imports: [
Card,
TableModule,
ButtonDirective,
Ripple,
Button,
RouterLink,
CatalogMovementReferenceTagComponent,
PriceMaskDirective,
JalaliDateDirective,
],
})
export class InventoryMovementListComponent {
@Input() inventoryId!: string;
@Input() movementType?: MovementType;
loading = signal(false);
movements = signal<IInventoryStockMovementResponse[]>([]);
expandedRows = {};
constructor(private service: InventoriesService) {}
get title() {
switch (this.movementType) {
case 'IN':
return 'گردش ورودی کالاها';
case 'OUT':
return 'گردش خروجی کالاها';
case 'ADJUST':
return 'گردش تعدیل کالاها';
default:
return 'تمام گردش کالاها';
}
}
ngOnInit() {
this.getAll();
}
getAll() {
this.loading.set(true);
console.log(this.inventoryId);
return this.service.getMovements(this.inventoryId, this.movementType).subscribe({
next: (res) => {
this.movements.set(res.data);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
}
}
@@ -3,7 +3,7 @@ import { UikitFieldComponent } from '@/uikit';
import { Component } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Select } from 'primeng/select';
import { IInventoryResponse } from '../../models';
import { IInventorySummaryResponse } from '../../models';
import { InventoriesService } from '../../services/main.service';
@Component({
@@ -11,7 +11,7 @@ import { InventoriesService } from '../../services/main.service';
templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent],
})
export class InventoriesSelectComponent extends AbstractSelectComponent<IInventoryResponse> {
export class InventoriesSelectComponent extends AbstractSelectComponent<IInventorySummaryResponse> {
constructor(private service: InventoriesService) {
super();
this.getData();
@@ -0,0 +1,149 @@
<p-card class="">
<ng-template #title> انتقال موجودی بین انبارها </ng-template>
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4 mt-4">
<div class="flex flex-col gap-4">
<div class="max-w-xs">
<app-input [control]="form.controls.code" label="شماره انتقال" />
</div>
<uikit-field [control]="form.controls.description" label="توضیحات">
<textarea
pTextarea
rows="4"
cols="30"
[formControl]="form.controls.description"
placeholder="توضیحات"
></textarea>
</uikit-field>
<div class="flex items-stretch gap-4">
<p-card class="flex-1 border border-primary-300">
<ng-template #title> انبار مبدا </ng-template>
<div class="mt-5">
<p-select
[options]="inventories()"
optionLabel="name"
[loading]="inventoriesLoading()"
[formControl]="form.controls.fromInventory"
placeholder="انتخاب انبار مبدا"
class="w-full"
></p-select>
<p-card class="mt-10 border border-primary-700">
<ng-template #title> کالاهای موجود در انبار مبدا </ng-template>
<div class="mt-5">
@if (!form.controls.fromInventory.value) {
<div class="text-center text-gray-500 py-10">لطفا انبار مبدا را انتخاب کنید</div>
} @else {
<p-table
[value]="originInventoryStock()"
[loading]="originInventoryStockLoading()"
selectionMode="multiple"
[(selection)]="selectedProducts"
(onRowSelect)="setSelectedProductsInForm($event)"
(onRowUnselect)="clearSelectedProductsInForm($event)"
dataKey="id"
class="w-full"
>
<ng-template #header>
<tr>
<th pSelectableRowCheckbox></th>
<th>نام کالا</th>
<th>موجودی</th>
<th>میانگین قیمت</th>
</tr>
</ng-template>
<ng-template let-item pTemplate="body">
<tr>
<td>
<p-tableCheckbox [disabled]="item.quantity === 0" [value]="item"></p-tableCheckbox>
</td>
<td>{{ item.product.name }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.avgCost }}</td>
</tr>
</ng-template>
</p-table>
}
</div>
</p-card>
</div>
</p-card>
<div class="shrink-0">
<div class="flex items-center h-100">
<i class="pi pi-arrow-left text-2xl"></i>
</div>
</div>
<p-card class="flex-1 border border-primary-300">
<ng-template #title> انبار مقصد </ng-template>
<div class="mt-5">
<p-select
[options]="toInventories"
optionLabel="name"
[loading]="inventoriesLoading()"
[formControl]="form.controls.toInventory"
placeholder="انتخاب انبار مقصد"
class="w-full"
></p-select>
<p-card class="mt-10 border border-primary-700">
<ng-template #title> کالاهای انتخاب شده </ng-template>
<div class="mt-5">
@if (!form.controls.fromInventory.value) {
<div class="text-center text-gray-500 py-10">لطفا انبار مبدا را انتخاب کنید</div>
} @else {
{{ form.controls.items.value.length }}
<p-table [value]="form.controls.items.controls" class="w-full">
<ng-template #header>
<tr>
<th>نام کالا</th>
<th>مقدار برای انتقال</th>
</tr>
</ng-template>
<ng-template let-item let-rowIndex="rowIndex" pTemplate="body">
<tr>
<td>{{ item.controls.product.value?.name }}</td>
<td>
<input
pInputText
type="number"
min="1"
[max]="item.controls.product.value?.quantity"
[formControl]="form.controls.items.at(rowIndex).controls.count"
/>
</td>
</tr>
</ng-template>
</p-table>
}
</div>
</p-card>
</div>
</p-card>
</div>
<div class="flex items-center gap-2">
<button
pButton
label="انصراف"
type="button"
outlined
icon="pi pi-times"
routerLink="/products"
[disabled]="form.disabled || submitLoading()"
></button>
<button
pButton
label="ذخیره"
type="submit"
icon="pi pi-check"
[loading]="submitLoading()"
(click)="submit()"
></button>
</div>
</div>
</form>
</p-card>
@@ -0,0 +1,157 @@
import { Maybe } from '@/core';
import { InputComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit';
import { Component, inject, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card';
import { InputText } from 'primeng/inputtext';
import { Select } from 'primeng/select';
import { TableModule, TableRowSelectEvent, TableRowUnSelectEvent } from 'primeng/table';
import { Textarea } from 'primeng/textarea';
import {
IInventoryProduct,
IInventoryStockResponse,
IInventorySummaryResponse,
IInventoryTransferRequest,
} from '../../models';
import { InventoriesService } from '../../services/main.service';
import { InventoriesSelectComponent } from '../select/select.component';
@Component({
selector: 'inventories-transfer-form',
templateUrl: './transfer-form.component.html',
imports: [
ReactiveFormsModule,
Card,
InventoriesSelectComponent,
InputComponent,
Textarea,
UikitFieldComponent,
Select,
TableModule,
InputText,
ButtonDirective,
],
})
export class InventoriesTransferFormComponent {
fb = inject(FormBuilder);
form = this.fb.group({
fromInventory: [null as Maybe<IInventorySummaryResponse>, [Validators.required]],
toInventory: [null as Maybe<IInventorySummaryResponse>, [Validators.required]],
code: ['', [Validators.required]],
description: [''],
items: this.fb.array(
[
this.fb.group({
product: [null as Maybe<IInventoryProduct>, [Validators.required]],
count: [1, [Validators.required, Validators.min(1)]],
}),
],
Validators.required,
),
});
inventoriesLoading = signal<boolean>(false);
inventories = signal<Maybe<IInventorySummaryResponse>[]>([]);
get toInventories() {
const fromInventoryId = this.form.controls.fromInventory.value;
return this.inventories().filter((inv) => inv?.id !== fromInventoryId?.id);
}
originInventoryStock = signal<any[]>([]);
originInventoryStockLoading = signal(false);
selectedProducts = signal<Maybe<IInventoryStockResponse>[]>([]);
constructor(private service: InventoriesService) {
this.form.controls.toInventory.disable();
this.form.controls.fromInventory.valueChanges.subscribe((val) => {
if (val && val.id) {
this.form.controls.toInventory.enable();
this.getSelectedInventoryStock();
}
});
this.getInventories();
}
setSelectedProductsInForm($e: TableRowSelectEvent<IInventoryStockResponse>) {
const stock = $e.data as Maybe<IInventoryStockResponse>;
if (stock && stock.product) {
if (
this.form.controls.items.length === 1 &&
!this.form.controls.items.value?.at(0)?.product
) {
this.form.controls.items.removeAt(0);
}
this.form.controls.items.push(
// @ts-ignore
this.fb.group({
product: [stock.product, [Validators.required]],
count: [1, [Validators.required, Validators.min(1)]],
}),
);
console.log(this.form.controls.items);
}
}
clearSelectedProductsInForm($e: TableRowUnSelectEvent<IInventoryProduct>) {
console.log($e.index);
if ($e.index != null) {
this.form.controls.items.removeAt($e.index);
}
}
getInventories() {
this.inventoriesLoading.set(true);
this.service.getAll().subscribe({
next: (res) => {
this.inventories.set(res.data);
this.inventoriesLoading.set(false);
},
error: () => {
this.inventoriesLoading.set(false);
},
});
}
getSelectedInventoryStock() {
this.originInventoryStockLoading.set(true);
this.service.getStock(String(this.form.controls.fromInventory.value?.id)).subscribe({
next: (res) => {
this.originInventoryStock.set(res.data);
this.originInventoryStockLoading.set(false);
},
error: () => {
this.originInventoryStockLoading.set(false);
},
});
}
submitLoading = signal(false);
submit() {
this.submitLoading.set(true);
const payload = {
fromInventoryId: this.form.controls.fromInventory.value?.id,
toInventoryId: this.form.controls.toInventory.value?.id,
code: this.form.controls.code.value || '',
description: this.form.controls.description.value || '',
items: this.form.controls.items.value.map((item) => ({
productId: item.product?.id,
count: item.count,
})),
} as IInventoryTransferRequest;
this.service.transfer(payload).subscribe({
next: () => {
this.submitLoading.set(false);
// this.form.reset();
},
error: () => {
this.submitLoading.set(false);
},
});
}
}
@@ -3,4 +3,9 @@ const baseUrl = '/api/v1/inventories';
export const INVENTORIES_API_ROUTES = {
list: () => `${baseUrl}`,
single: (inventoryId: string) => `${baseUrl}/${inventoryId}`,
inventoryMovements: (inventoryId: string) => `${baseUrl}/${inventoryId}/movements`,
transfer: () => `${baseUrl}/transfers`,
stock: (inventoryId: string) => `${baseUrl}/${inventoryId}/stock`,
productCardex: (inventoryId: string, productId: string) =>
`${baseUrl}/${inventoryId}/products/${productId}/cardex`,
};
@@ -1,7 +1,12 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
export type TInventoriesRouteNames = 'inventories' | 'inventory';
export type TInventoriesRouteNames =
| 'inventories'
| 'inventory'
| 'transfer'
| 'products'
| 'productCardex';
export const inventoriesNamedRoutes: NamedRoutes<TInventoriesRouteNames> = {
inventories: {
@@ -12,6 +17,16 @@ export const inventoriesNamedRoutes: NamedRoutes<TInventoriesRouteNames> = {
pagePath: () => '/inventories',
},
},
transfer: {
path: 'inventories/transfer',
loadComponent: () =>
import('../../views/transfer.component').then((m) => m.InventoriesTransferComponent),
meta: {
title: 'انتقال بین انبارها',
pagePath: () => '/inventories/transfer',
},
},
inventory: {
path: 'inventories/:inventoryId',
loadComponent: () => import('../../views/single.component').then((m) => m.InventoryComponent),
@@ -20,6 +35,24 @@ export const inventoriesNamedRoutes: NamedRoutes<TInventoriesRouteNames> = {
pagePath: () => '/inventories/:inventoryId',
},
},
products: {
path: 'inventories/:inventoryId/products',
loadComponent: () =>
import('../../views/products.component').then((m) => m.InventoryProductsComponent),
meta: {
title: 'محصولات انبار',
pagePath: () => '/inventories/:inventoryId/products',
},
},
productCardex: {
path: 'inventories/:inventoryId/products/:productId/cardex',
loadComponent: () =>
import('../../views/product-cardex.component').then((m) => m.InventoryProductCardexComponent),
meta: {
title: 'کارتکس محصول',
pagePath: () => '/inventories/:inventoryId/products/:productId/cardex',
},
},
};
export const INVENTORIES_ROUTES: Routes = Object.values(inventoriesNamedRoutes);
@@ -1 +1,2 @@
export * from './io';
export * from './types';
+54 -2
View File
@@ -1,14 +1,66 @@
export interface IInventoryRawResponse {
import {
IInventoryInfo,
IInventoryMovement,
IInventoryStockProduct,
IInventoryTransferRequestItem,
} from './types';
export interface IInventorySummaryRawResponse {
id: number;
name: string;
location: string;
isActive: boolean;
isPointOfSale: boolean;
createdAt: string;
updatedAt: string;
deletedAt: Maybe<string>;
}
export interface IInventoryResponse extends IInventoryRawResponse {}
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;
location: string;
isActive: boolean;
}
export interface IInventoryStockMovementRawResponse {
receiptId: string;
count: number;
info: IInventoryInfo;
movements: IInventoryMovement[];
}
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 {}
@@ -0,0 +1,42 @@
import { IProductRawResponse } from '@/modules/products/models';
export interface IInventoryMovement {
id: number;
quantity: number;
fee: number;
totalCost: number;
avgCost: number;
product: IInventoryProduct;
remainedInStock: number;
}
export interface IInventoryProduct {
id: number;
name: string;
description: null;
sku: string;
barcode: string;
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 {}
@@ -1,9 +1,24 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { MovementType } from '@/shared/catalog';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { INVENTORIES_API_ROUTES } from '../constants';
import { IInventoryRawResponse, IInventoryRequest, IInventoryResponse } from '../models';
import {
IInventoryDetailRawResponse,
IInventoryDetailResponse,
IInventoryProductCardexRawResponse,
IInventoryProductCardexResponse,
IInventoryRequest,
IInventoryStockMovementRawResponse,
IInventoryStockMovementResponse,
IInventoryStockQuery,
IInventoryStockRawResponse,
IInventoryStockResponse,
IInventorySummaryRawResponse,
IInventorySummaryResponse,
IInventoryTransferRequest,
} from '../models';
@Injectable({ providedIn: 'root' })
export class InventoriesService {
@@ -11,15 +26,50 @@ export class InventoriesService {
private apiRoutes = INVENTORIES_API_ROUTES;
getAll(): Observable<IPaginatedResponse<IInventoryResponse>> {
return this.http.get<IPaginatedResponse<IInventoryRawResponse>>(this.apiRoutes.list());
getAll(): Observable<IPaginatedResponse<IInventorySummaryResponse>> {
return this.http.get<IPaginatedResponse<IInventorySummaryRawResponse>>(this.apiRoutes.list());
}
getSingle(inventoryId: string): Observable<IInventoryResponse> {
return this.http.get<IInventoryRawResponse>(this.apiRoutes.single(inventoryId));
getSingle(inventoryId: string): Observable<IInventoryDetailResponse> {
return this.http.get<IInventoryDetailRawResponse>(this.apiRoutes.single(inventoryId));
}
create(data: IInventoryRequest): Observable<IInventoryResponse> {
return this.http.post<IInventoryRawResponse>(this.apiRoutes.list(), data);
create(data: IInventoryRequest): Observable<IInventorySummaryResponse> {
return this.http.post<IInventorySummaryRawResponse>(this.apiRoutes.list(), data);
}
getMovements(
inventoryId: string,
movementType?: MovementType,
): Observable<IPaginatedResponse<IInventoryStockMovementResponse>> {
return this.http.get<IPaginatedResponse<IInventoryStockMovementRawResponse>>(
this.apiRoutes.inventoryMovements(inventoryId),
{
params: movementType ? { type: movementType } : {},
},
);
}
transfer(payload: IInventoryTransferRequest): Observable<void> {
return this.http.post<void>(this.apiRoutes.transfer(), payload);
}
getStock(
inventoryId: string,
params?: IInventoryStockQuery,
): Observable<IPaginatedResponse<IInventoryStockResponse>> {
return this.http.get<IPaginatedResponse<IInventoryStockRawResponse>>(
this.apiRoutes.stock(inventoryId),
{ params: params ? { ...params } : {} },
);
}
getProductCardex(
inventoryId: string,
productId: string,
): Observable<IPaginatedResponse<IInventoryProductCardexResponse>> {
return this.http.get<IPaginatedResponse<IInventoryProductCardexRawResponse>>(
`${this.apiRoutes.productCardex(inventoryId, productId)}`,
);
}
}
@@ -8,6 +8,7 @@
[items]="items()"
[loading]="loading()"
[showDetails]="true"
(onDetails)="toDetails($event)"
(onAdd)="openAddForm()"
/>
@@ -2,9 +2,10 @@ import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { Component, signal } from '@angular/core';
import { Component, inject, signal } from '@angular/core';
import { Router } from '@angular/router';
import { InventoryFormComponent } from '../components/form.component';
import { IInventoryResponse } from '../models';
import { IInventorySummaryResponse } from '../models';
import { InventoriesService } from '../services/main.service';
@Component({
@@ -13,6 +14,7 @@ import { InventoriesService } from '../services/main.service';
imports: [PageDataListComponent, InventoryFormComponent],
})
export class InventoriesComponent {
private router = inject(Router);
constructor(private inventoryService: InventoriesService) {
this.getData();
}
@@ -21,11 +23,12 @@ export class InventoriesComponent {
{ field: 'id', header: 'شناسه' },
{ field: 'name', header: 'نام' },
{ field: 'location', header: 'موقعیت' },
{ field: 'isPointOfSale', header: 'نقطه فروش' },
{ field: 'isActive', header: 'فعال' },
] as IColumn[];
loading = signal(false);
items = signal<IInventoryResponse[]>([]);
items = signal<IInventorySummaryResponse[]>([]);
visibleForm = signal(false);
refresh() {
@@ -43,4 +46,8 @@ export class InventoriesComponent {
openAddForm() {
this.visibleForm.set(true);
}
toDetails(item: IInventorySummaryResponse) {
this.router.navigate(['/inventories', item.id]);
}
}
@@ -0,0 +1,16 @@
@if (pageLoading) {
<div class="h-svh w-svw flex items-center justify-center">
<p-progress-spinner />
</div>
} @else {
<p-card>
<ng-template #header>
<div class="p-4 flex items-center justify-between">
<span class="text-xl font-bold">کاردکس کالای {{ product()?.name }} در انبار {{ inventory()?.name }}</span>
<!-- <button pButton outlined icon="pi pi-refresh" [loading]="get()" (click)="getData()"></button> -->
</div>
</ng-template>
<shared-cardex [loading]="getCardexLoading()" [items]="cardex() || []" />
</p-card>
}
@@ -0,0 +1,80 @@
import { Maybe } from '@/core';
import { IProductResponse } from '@/modules/products/models';
import { ProductsService } from '@/modules/products/services/main.service';
import { CardexComponent } from '@/shared/components/cardex/cardex.component';
import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Card } from 'primeng/card';
import { ProgressSpinner } from 'primeng/progressspinner';
import { IInventoryDetailResponse, IInventoryProductCardexResponse } from '../models';
import { InventoriesService } from '../services/main.service';
@Component({
selector: 'inventory-product-cardex',
templateUrl: './product-cardex.component.html',
imports: [ProgressSpinner, Card, CardexComponent],
})
export class InventoryProductCardexComponent {
private route = inject(ActivatedRoute);
constructor(
private service: InventoriesService,
private productService: ProductsService,
) {
this.getInventory();
this.getProduct();
this.getCardex();
}
inventoryId = this.route.snapshot.params['inventoryId'];
productId = this.route.snapshot.params['productId'];
getInventoryLoading = signal(false);
inventory = signal<Maybe<IInventoryDetailResponse>>(null);
getProductLoading = signal(false);
product = signal<Maybe<IProductResponse>>(null);
getCardexLoading = signal(false);
cardex = signal<Maybe<IInventoryProductCardexResponse[]>>(null);
get pageLoading() {
return this.getInventoryLoading() || this.getProductLoading();
}
getInventory() {
this.getInventoryLoading.set(true);
this.service.getSingle(this.inventoryId).subscribe({
next: (res) => {
this.inventory.set(res);
this.getInventoryLoading.set(false);
},
error: () => {
this.getInventoryLoading.set(false);
},
});
}
getProduct() {
this.getProductLoading.set(true);
this.productService.getSingle(this.productId).subscribe({
next: (res) => {
this.getProductLoading.set(false);
this.product.set(res);
},
error: () => {
this.getProductLoading.set(false);
},
});
}
getCardex() {
this.service.getProductCardex(this.inventoryId, this.productId).subscribe({
next: (res) => {
this.getCardexLoading.set(false);
this.cardex.set(res.data);
},
error: () => {
this.getCardexLoading.set(false);
},
});
}
}
@@ -0,0 +1,51 @@
<p-card class="">
<ng-template pTemplate="header">
<div class="p-4 flex items-center justify-between">
<span class="text-xl font-bold">کالاهای موجود در انبار</span>
<button pButton outlined icon="pi pi-refresh" [loading]="loading()" (click)="getData()"></button>
</div>
</ng-template>
<p-table [value]="data()" [loading]="loading()">
<ng-template #header>
<tr>
<th pSortableColumn="product.id">
<div class="flex items-center gap-2">
شناسه کالا
<p-sortIcon field="product.id" />
</div>
</th>
<th pSortableColumn="product.name">
<div class="flex items-center gap-2">
عنوان کالا
<p-sortIcon field="product.name" />
</div>
</th>
<th pSortableColumn="quantity">
<div class="flex items-center gap-2">
تعداد
<p-sortIcon field="quantity" />
</div>
</th>
<th pSortableColumn="fee">
<div class="flex items-center gap-2">
قیمت متوسط
<p-sortIcon field="fee" />
</div>
</th>
<th style="width: 4rem"></th>
</tr>
</ng-template>
<ng-template #body let-stock>
<tr>
<td>{{ stock.product.sku }}</td>
<td>{{ stock.product.name }}</td>
<td>{{ stock.quantity }}</td>
<td><span [appPriceMask]="stock.avgCost"></span></td>
<td>
<button pButton type="button" icon="pi pi-search" title="کاردکس کالا"></button>
</td>
</tr>
</ng-template>
</p-table>
</p-card>
@@ -0,0 +1,37 @@
import { PriceMaskDirective } from '@/shared/directives';
import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card';
import { TableModule } from 'primeng/table';
import { IInventoryStockResponse } from '../models';
import { InventoriesService } from '../services/main.service';
@Component({
selector: 'inventory-products',
templateUrl: './products.component.html',
imports: [Card, TableModule, ButtonDirective, PriceMaskDirective],
})
export class InventoryProductsComponent {
private route = inject(ActivatedRoute);
constructor(private service: InventoriesService) {
this.getData();
}
inventoryId = this.route.snapshot.params['inventoryId'];
loading = signal(false);
data = signal<IInventoryStockResponse[]>([]);
getData() {
this.loading.set(true);
this.service.getStock(this.inventoryId).subscribe({
next: (res) => {
this.data.set(res.data);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
}
}
@@ -1 +1,96 @@
<div class=""></div>
<div class="flex flex-col gap-4">
<div class="flex items-center">
<div class="grow">
<div class="flex items-center gap-2">
<p-button icon="pi pi-arrow-right" aria-label="بازگشت" outlined [routerLink]="['/inventories']" />
<h3 class="m-0">{{ data()?.name }}</h3>
</div>
</div>
<div class="flex items-center gap-2">
<button pButton type="button" label="افزایش موجودی" icon="pi pi-plus"></button>
<!-- [routerLink]="['/inventories', inventoryId, 'purchase']" -->
<button
pButton
type="button"
label="انتقال کالا"
icon="pi pi-plus"
[routerLink]="['/inventories', inventoryId, 'transfer']"
></button>
<button pButton type="button" label="ویرایش" icon="pi pi-pencil"></button>
<!-- [routerLink]="['/inventories', inventoryId, 'update']" -->
<button pButton icon="pi pi-trash" severity="danger" (click)="openDeleteConfirm()"></button>
</div>
</div>
<div class="inline-flex items-center gap-5">
<div class="">
<app-key-value label="موقعیت انبار" [value]="data()?.location || '-'" />
</div>
<div class="">
<app-key-value label="قابلیت فروش" [value]="data()?.isPointOfSale ? 'دارد' : 'ندارد'" />
</div>
</div>
<div class="mt-5">
<div class="grid grid-cols-4 gap-4">
@for (item of topBarCardDetails; track $index) {
<details-info-card [icon]="item.icon" [label]="item.label" [value]="item.value?.toString() || ''" />
}
</div>
</div>
<p-card class="">
<ng-template pTemplate="header">
<div class="p-4 flex items-center justify-between">
<span class="text-xl font-bold">کالاهای موجود در انبار</span>
<button pButton outlined icon="pi pi-refresh" [loading]="loading()" (click)="getStock()"></button>
</div>
</ng-template>
<p-table [value]="stock()" [loading]="getStockLoading()">
<ng-template #header>
<tr>
<th pSortableColumn="product.id">
<div class="flex items-center gap-2">
شناسه کالا
<p-sortIcon field="product.id" />
</div>
</th>
<th pSortableColumn="product.name">
<div class="flex items-center gap-2">
عنوان کالا
<p-sortIcon field="product.name" />
</div>
</th>
<th pSortableColumn="quantity">
<div class="flex items-center gap-2">
تعداد
<p-sortIcon field="quantity" />
</div>
</th>
<th pSortableColumn="fee">
<div class="flex items-center gap-2">
قیمت متوسط
<p-sortIcon field="fee" />
</div>
</th>
<th style="width: 4rem"></th>
</tr>
</ng-template>
<ng-template #body let-stock>
<tr>
<td>{{ stock.product.sku }}</td>
<td>{{ stock.product.name }}</td>
<td>{{ stock.quantity }}</td>
<td><span [appPriceMask]="stock.avgCost"></span></td>
<td>
<p-button type="button" icon="pi pi-search" title="کاردکس کالا" />
</td>
</tr>
</ng-template>
</p-table>
</p-card>
<inventory-movement-list [inventoryId]="inventoryId" movementType="IN" />
<inventory-movement-list [inventoryId]="inventoryId" movementType="OUT" />
<inventory-movement-list [inventoryId]="inventoryId" movementType="ADJUST" />
</div>
@@ -1,12 +1,15 @@
import { Maybe } from '@/core';
import { ProductChargeFormComponent } from '@/modules/productCharges/components/form.component';
import { KeyValueComponent } from '@/shared/components';
import { DetailsInfoCardComponent } from '@/shared/components/detailsInfoCard/details-info-card.component';
import { PriceMaskDirective } from '@/shared/directives';
import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { Button, ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card';
import { GalleriaModule } from 'primeng/galleria';
import { IInventoryResponse } from '../models';
import { TableModule } from 'primeng/table';
import { InventoryMovementListComponent } from '../components/movementList/movement-list.component';
import { IInventoryDetailResponse, IInventoryStockResponse } from '../models';
import { InventoriesService } from '../services/main.service';
@Component({
@@ -17,25 +20,28 @@ import { InventoriesService } from '../services/main.service';
RouterLink,
GalleriaModule,
KeyValueComponent,
Card,
Button,
ProductChargeFormComponent,
InventoryMovementListComponent,
DetailsInfoCardComponent,
Card,
TableModule,
PriceMaskDirective,
],
})
export class InventoryComponent {
private route = inject(ActivatedRoute);
constructor(private service: InventoriesService) {
this.getData();
this.getStock();
}
productId = this.route.snapshot.params['productId'];
isOpenChargeForm = signal(false);
inventoryId = this.route.snapshot.params['inventoryId'];
loading = signal(false);
data = signal<Maybe<IInventoryResponse>>(null);
data = signal<Maybe<IInventoryDetailResponse>>(null);
getData() {
this.loading.set(true);
this.service.getSingle(this.productId).subscribe({
this.service.getSingle(this.inventoryId).subscribe({
next: (res) => {
this.data.set(res);
this.loading.set(false);
@@ -46,35 +52,41 @@ export class InventoryComponent {
});
}
getStockLoading = signal(false);
stock = signal<IInventoryStockResponse[]>([]);
getStock() {
this.getStockLoading.set(true);
this.service.getStock(this.inventoryId, { isAvailable: true }).subscribe({
next: (res) => {
this.getStockLoading.set(false);
this.stock.set(res.data);
},
error: () => {
this.getStockLoading.set(false);
},
});
}
get topBarCardDetails() {
return [
// {
// icon: 'pi pi-box',
// label: 'موجودی',
// value: this.data()? || 0,
// },
// {
// icon: 'pi pi-dollar',
// label: 'قیمت پایه',
// value: this.data()?.basePrice?.toString() || 'تعیین نشده',
// },
// {
// icon: 'pi pi-calendar',
// label: 'تاریخ ایجاد',
// value: this.data()?.createdAt
// ? new Date(this.data()!.createdAt!).toLocaleDateString()
// : '-',
// },
// {
// icon: 'pi pi-clock',
// label: 'تعداد فروش',
// value: 1200,
// },
{
icon: 'pi pi-box',
label: 'تعداد کالاهای موجود',
value: this.data()?.availableProductCount,
},
{
icon: 'pi pi-dollar',
label: 'تنوع کالاها',
value: this.data()?.availableProductTypes,
},
{
icon: 'pi pi-dollar',
label: 'ارزش کلی کالاهای موجود',
value: this.data()?.availableProductsCost,
},
];
}
openDeleteConfirm() {}
openChargeForm() {
this.isOpenChargeForm.set(true);
}
}
@@ -0,0 +1,3 @@
<div class="max-w-6xl mx-auto">
<inventories-transfer-form></inventories-transfer-form>
</div>
@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
import { InventoriesTransferFormComponent } from '../components/transfer/transfer-form.component';
@Component({
selector: 'inventories-transfer',
templateUrl: './transfer.component.html',
imports: [InventoriesTransferFormComponent],
})
export class InventoriesTransferComponent {
constructor() {}
}