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:
@@ -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
@@ -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() {}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// const baseUrl = '/api/v1/product-categories';
|
||||
|
||||
// export const PRODUCT_CATEGORIES_API_ROUTES = {
|
||||
// list: () => `${baseUrl}`,
|
||||
// single: (categoryId: string) => `${baseUrl}/${categoryId}`,
|
||||
// };
|
||||
@@ -0,0 +1,2 @@
|
||||
// export * from './apiRoutes';
|
||||
export * from './routes';
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NamedRoutes } from '@/core';
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
export type TPOSRouteNames = 'POS';
|
||||
|
||||
export const posNamedRoutes: NamedRoutes<TPOSRouteNames> = {
|
||||
POS: {
|
||||
path: 'pos',
|
||||
loadComponent: () => import('../../views/pos.component').then((m) => m.POSComponent),
|
||||
meta: {
|
||||
title: 'نقطه فروش',
|
||||
pagePath: () => '/pos',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const POS_ROUTES: Routes = Object.values(posNamedRoutes);
|
||||
@@ -0,0 +1 @@
|
||||
<div class=""></div>
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pos',
|
||||
templateUrl: './pos.component.html',
|
||||
})
|
||||
export class POSComponent {
|
||||
constructor() {}
|
||||
}
|
||||
+2
@@ -16,6 +16,8 @@ export interface IProductRawResponse {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
deletedAt?: Maybe<Date>;
|
||||
stock: number;
|
||||
avgCost?: number;
|
||||
}
|
||||
|
||||
export interface IProductResponse extends IProductRawResponse {}
|
||||
|
||||
@@ -30,17 +30,7 @@
|
||||
<div class="flex grow flex-col gap-4">
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
@for (item of topBarCardDetails; track $index) {
|
||||
<p-card class="w-full border border-primary-700">
|
||||
<div class="flex gap-5 text-primary-400">
|
||||
<div class="shrink-0 pt-1">
|
||||
<i [class]="'pi ' + item.icon + ' text-2xl'"></i>
|
||||
</div>
|
||||
<div class="grow flex flex-col gap-1">
|
||||
<span class="text-sm">{{ item.label }}</span>
|
||||
<span class="text-xl font-bold text-primary">{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</p-card>
|
||||
<details-info-card [icon]="item.icon" [label]="item.label" [value]="item.value.toString()" />
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Maybe } from '@/core';
|
||||
import { PurchaseFormComponent } from '@/modules/purchases/components/form.component';
|
||||
import { KeyValueComponent } from '@/shared/components';
|
||||
import { DetailsInfoCardComponent } from '@/shared/components/detailsInfoCard/details-info-card.component';
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { Button, ButtonDirective } from 'primeng/button';
|
||||
@@ -20,6 +21,7 @@ import { ProductsService } from '../services/main.service';
|
||||
Card,
|
||||
Button,
|
||||
PurchaseFormComponent,
|
||||
DetailsInfoCardComponent,
|
||||
],
|
||||
})
|
||||
export class ProductComponent {
|
||||
@@ -79,7 +81,7 @@ export class ProductComponent {
|
||||
{
|
||||
icon: 'pi pi-box',
|
||||
label: 'موجودی',
|
||||
value: this.product()?.count || 0,
|
||||
value: this.product()?.stock || 0,
|
||||
},
|
||||
{
|
||||
icon: 'pi pi-dollar',
|
||||
@@ -87,16 +89,14 @@ export class ProductComponent {
|
||||
value: this.product()?.basePrice?.toString() || 'تعیین نشده',
|
||||
},
|
||||
{
|
||||
icon: 'pi pi-calendar',
|
||||
label: 'تاریخ ایجاد',
|
||||
value: this.product()?.createdAt
|
||||
? new Date(this.product()!.createdAt!).toLocaleDateString()
|
||||
: '-',
|
||||
icon: 'pi pi-dollar',
|
||||
label: 'میانگین قیمت',
|
||||
value: this.product()?.avgCost?.toString() || 'تعیین نشده',
|
||||
},
|
||||
{
|
||||
icon: 'pi pi-clock',
|
||||
label: 'تعداد فروش',
|
||||
value: 1200,
|
||||
value: '-',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { InventoriesSelectComponent } from '@/modules/inventories/components/select/select.component';
|
||||
import { IInventoryResponse } from '@/modules/inventories/models';
|
||||
import { IInventorySummaryResponse } from '@/modules/inventories/models';
|
||||
import { IProductResponse } from '@/modules/products/models';
|
||||
import { SuppliersSelectComponent } from '@/modules/suppliers/components/select/select.component';
|
||||
import { ISupplierResponse } from '@/modules/suppliers/models';
|
||||
@@ -30,7 +30,7 @@ import { ProductChargeRowFormComponent } from './product-charge-row-form.compone
|
||||
})
|
||||
export class PurchaseFormComponent {
|
||||
@Input() product?: IProductResponse;
|
||||
@Input() inventory?: IInventoryResponse;
|
||||
@Input() inventory?: IInventorySummaryResponse;
|
||||
@Input() supplier?: ISupplierResponse;
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
+5
-4
@@ -1,7 +1,7 @@
|
||||
import { Maybe } from '@/core';
|
||||
import { ToastService } from '@/core/services/toast.service';
|
||||
import { InventoriesSelectComponent } from '@/modules/inventories/components/select/select.component';
|
||||
import { IInventoryResponse } from '@/modules/inventories/models';
|
||||
import { IInventorySummaryResponse } from '@/modules/inventories/models';
|
||||
import { IProductResponse } from '@/modules/products/models';
|
||||
import { SuppliersSelectComponent } from '@/modules/suppliers/components/select/select.component';
|
||||
import { ISupplierResponse } from '@/modules/suppliers/models';
|
||||
@@ -44,7 +44,7 @@ export class PurchaseReceiptTemplateComponent {
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
@Input() product?: IProductResponse;
|
||||
@Input() inventory?: IInventoryResponse;
|
||||
@Input() inventory?: IInventorySummaryResponse;
|
||||
@Input() supplier?: ISupplierResponse;
|
||||
|
||||
columns = [
|
||||
@@ -105,7 +105,7 @@ export class PurchaseReceiptTemplateComponent {
|
||||
total: [300000, [Validators.required, Validators.min(0)]],
|
||||
}),
|
||||
]),
|
||||
inventory: [{ id: 2 } as Maybe<IInventoryResponse>, [Validators.required]],
|
||||
inventory: [{ id: 2 } as Maybe<IInventorySummaryResponse>, [Validators.required]],
|
||||
supplier: [{ id: 1 } as Maybe<ISupplierResponse>, [Validators.required]],
|
||||
});
|
||||
|
||||
@@ -179,7 +179,8 @@ export class PurchaseReceiptTemplateComponent {
|
||||
next: (res) => {
|
||||
this.toastService.success({ text: 'ثبت سند خرید با موفقیت انجام شد' });
|
||||
this.form.enable();
|
||||
this.form.reset();
|
||||
// this.form.reset();
|
||||
this.form.controls.code.setValue(this.form.controls.code.value + '1');
|
||||
this.formIsSubmitted.set(false);
|
||||
},
|
||||
error: () => {
|
||||
|
||||
Reference in New Issue
Block a user