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:
@@ -12,8 +12,10 @@
|
|||||||
"@primeuix/themes": "^1.2.1",
|
"@primeuix/themes": "^1.2.1",
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"chart.js": "4.4.2",
|
"chart.js": "4.4.2",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
"flatpickr-wrap": "^1.0.0",
|
"flatpickr-wrap": "^1.0.0",
|
||||||
|
"jalaliday": "^3.1.1",
|
||||||
"jest-editor-support": "*",
|
"jest-editor-support": "*",
|
||||||
"primeclt": "^0.1.5",
|
"primeclt": "^0.1.5",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
|
|||||||
Generated
+20
@@ -44,12 +44,18 @@ importers:
|
|||||||
chart.js:
|
chart.js:
|
||||||
specifier: 4.4.2
|
specifier: 4.4.2
|
||||||
version: 4.4.2
|
version: 4.4.2
|
||||||
|
dayjs:
|
||||||
|
specifier: ^1.11.19
|
||||||
|
version: 1.11.19
|
||||||
flatpickr:
|
flatpickr:
|
||||||
specifier: ^4.6.13
|
specifier: ^4.6.13
|
||||||
version: 4.6.13
|
version: 4.6.13
|
||||||
flatpickr-wrap:
|
flatpickr-wrap:
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
jalaliday:
|
||||||
|
specifier: ^3.1.1
|
||||||
|
version: 3.1.1(dayjs@1.11.19)
|
||||||
jest-editor-support:
|
jest-editor-support:
|
||||||
specifier: '*'
|
specifier: '*'
|
||||||
version: 32.0.0-beta.1
|
version: 32.0.0-beta.1
|
||||||
@@ -3136,6 +3142,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==}
|
resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
|
dayjs@1.11.19:
|
||||||
|
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
|
||||||
|
|
||||||
debug@2.6.9:
|
debug@2.6.9:
|
||||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4200,6 +4209,11 @@ packages:
|
|||||||
jackspeak@3.4.3:
|
jackspeak@3.4.3:
|
||||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||||
|
|
||||||
|
jalaliday@3.1.1:
|
||||||
|
resolution: {integrity: sha512-exZzDOfp/32Gj2YkEHNhBBMcltBCTrIe8RXLTfpCvdjYHNfbYSQiNeFVf4RR59FV+2fixUOCEFfzOzyXfgenlg==}
|
||||||
|
peerDependencies:
|
||||||
|
dayjs: ^1.11.13
|
||||||
|
|
||||||
jasmine-core@4.6.1:
|
jasmine-core@4.6.1:
|
||||||
resolution: {integrity: sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==}
|
resolution: {integrity: sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==}
|
||||||
|
|
||||||
@@ -9374,6 +9388,8 @@ snapshots:
|
|||||||
|
|
||||||
date-format@4.0.14: {}
|
date-format@4.0.14: {}
|
||||||
|
|
||||||
|
dayjs@1.11.19: {}
|
||||||
|
|
||||||
debug@2.6.9:
|
debug@2.6.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.0.0
|
ms: 2.0.0
|
||||||
@@ -10638,6 +10654,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@pkgjs/parseargs': 0.11.0
|
'@pkgjs/parseargs': 0.11.0
|
||||||
|
|
||||||
|
jalaliday@3.1.1(dayjs@1.11.19):
|
||||||
|
dependencies:
|
||||||
|
dayjs: 1.11.19
|
||||||
|
|
||||||
jasmine-core@4.6.1: {}
|
jasmine-core@4.6.1: {}
|
||||||
|
|
||||||
jasmine-core@5.8.0: {}
|
jasmine-core@5.8.0: {}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CUSTOMERS_ROUTES } from '@/modules/customers/constants';
|
import { CUSTOMERS_ROUTES } from '@/modules/customers/constants';
|
||||||
import { INVENTORIES_ROUTES } from '@/modules/inventories/constants';
|
import { INVENTORIES_ROUTES } from '@/modules/inventories/constants';
|
||||||
|
import { POSComponent } from '@/modules/pos/views/pos.component';
|
||||||
import { PRODUCT_BRANDS_ROUTES } from '@/modules/productBrands/constants';
|
import { PRODUCT_BRANDS_ROUTES } from '@/modules/productBrands/constants';
|
||||||
import { PRODUCT_CATEGORIES_ROUTES } from '@/modules/productCategories/constants';
|
import { PRODUCT_CATEGORIES_ROUTES } from '@/modules/productCategories/constants';
|
||||||
import { PRODUCTS_ROUTES } from '@/modules/products/constants';
|
import { PRODUCTS_ROUTES } from '@/modules/products/constants';
|
||||||
@@ -31,6 +32,10 @@ export const appRoutes: Routes = [
|
|||||||
{ path: 'pages', loadChildren: () => import('./app/pages/pages.routes') },
|
{ path: 'pages', loadChildren: () => import('./app/pages/pages.routes') },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'pos',
|
||||||
|
component: POSComponent,
|
||||||
|
},
|
||||||
{ path: 'notfound', component: Notfound },
|
{ path: 'notfound', component: Notfound },
|
||||||
{ path: 'auth', loadChildren: () => import('./app/pages/auth/auth.routes') },
|
{ path: 'auth', loadChildren: () => import('./app/pages/auth/auth.routes') },
|
||||||
{ path: '**', redirectTo: '/notfound' },
|
{ path: '**', redirectTo: '/notfound' },
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ export const MENU_ITEMS = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'موجودیها',
|
label: 'انبارها',
|
||||||
icon: 'pi pi-fw pi-box',
|
icon: 'pi pi-fw pi-box',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: 'لیست موجودیها',
|
label: 'لیست انبارها',
|
||||||
icon: 'pi pi-fw pi-list',
|
icon: 'pi pi-fw pi-list',
|
||||||
routerLink: ['/inventories'],
|
routerLink: ['/inventories'],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export * from './maybe.model';
|
|||||||
export * from './namedRoutes.model';
|
export * from './namedRoutes.model';
|
||||||
export * from './navigation.model';
|
export * from './navigation.model';
|
||||||
export * from './route-utils.model';
|
export * from './route-utils.model';
|
||||||
|
export * from './tag';
|
||||||
export * from './user.model';
|
export * from './user.model';
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export interface StockBalance {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export type TagSeverity =
|
||||||
|
| 'success'
|
||||||
|
| 'secondary'
|
||||||
|
| 'info'
|
||||||
|
| 'warn'
|
||||||
|
| 'danger'
|
||||||
|
| 'contrast'
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
@@ -4,7 +4,7 @@ import { FormFooterActionsComponent } from '@/shared/components/formFooterAction
|
|||||||
import { Component, effect, EventEmitter, inject, Input, Output, signal } from '@angular/core';
|
import { Component, effect, EventEmitter, inject, Input, Output, signal } from '@angular/core';
|
||||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { Dialog } from 'primeng/dialog';
|
import { Dialog } from 'primeng/dialog';
|
||||||
import { IInventoryRequest, IInventoryResponse } from '../models';
|
import { IInventoryRequest, IInventorySummaryResponse } from '../models';
|
||||||
import { InventoriesService } from '../services/main.service';
|
import { InventoriesService } from '../services/main.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -13,7 +13,7 @@ import { InventoriesService } from '../services/main.service';
|
|||||||
imports: [ReactiveFormsModule, Dialog, InputComponent, FormFooterActionsComponent],
|
imports: [ReactiveFormsModule, Dialog, InputComponent, FormFooterActionsComponent],
|
||||||
})
|
})
|
||||||
export class InventoryFormComponent {
|
export class InventoryFormComponent {
|
||||||
@Input() initialValues?: IInventoryResponse;
|
@Input() initialValues?: IInventorySummaryResponse;
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
set visible(v: boolean) {
|
set visible(v: boolean) {
|
||||||
@@ -25,7 +25,7 @@ export class InventoryFormComponent {
|
|||||||
private visibleSignal = signal(false);
|
private visibleSignal = signal(false);
|
||||||
|
|
||||||
@Output() visibleChange = new EventEmitter<boolean>();
|
@Output() visibleChange = new EventEmitter<boolean>();
|
||||||
@Output() onSubmit = new EventEmitter<IInventoryResponse>();
|
@Output() onSubmit = new EventEmitter<IInventorySummaryResponse>();
|
||||||
|
|
||||||
private fb = inject(FormBuilder);
|
private fb = inject(FormBuilder);
|
||||||
constructor(
|
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 { Component } from '@angular/core';
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { Select } from 'primeng/select';
|
import { Select } from 'primeng/select';
|
||||||
import { IInventoryResponse } from '../../models';
|
import { IInventorySummaryResponse } from '../../models';
|
||||||
import { InventoriesService } from '../../services/main.service';
|
import { InventoriesService } from '../../services/main.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -11,7 +11,7 @@ import { InventoriesService } from '../../services/main.service';
|
|||||||
templateUrl: './select.component.html',
|
templateUrl: './select.component.html',
|
||||||
imports: [ReactiveFormsModule, Select, UikitFieldComponent],
|
imports: [ReactiveFormsModule, Select, UikitFieldComponent],
|
||||||
})
|
})
|
||||||
export class InventoriesSelectComponent extends AbstractSelectComponent<IInventoryResponse> {
|
export class InventoriesSelectComponent extends AbstractSelectComponent<IInventorySummaryResponse> {
|
||||||
constructor(private service: InventoriesService) {
|
constructor(private service: InventoriesService) {
|
||||||
super();
|
super();
|
||||||
this.getData();
|
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 = {
|
export const INVENTORIES_API_ROUTES = {
|
||||||
list: () => `${baseUrl}`,
|
list: () => `${baseUrl}`,
|
||||||
single: (inventoryId: string) => `${baseUrl}/${inventoryId}`,
|
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 { NamedRoutes } from '@/core';
|
||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
export type TInventoriesRouteNames = 'inventories' | 'inventory';
|
export type TInventoriesRouteNames =
|
||||||
|
| 'inventories'
|
||||||
|
| 'inventory'
|
||||||
|
| 'transfer'
|
||||||
|
| 'products'
|
||||||
|
| 'productCardex';
|
||||||
|
|
||||||
export const inventoriesNamedRoutes: NamedRoutes<TInventoriesRouteNames> = {
|
export const inventoriesNamedRoutes: NamedRoutes<TInventoriesRouteNames> = {
|
||||||
inventories: {
|
inventories: {
|
||||||
@@ -12,6 +17,16 @@ export const inventoriesNamedRoutes: NamedRoutes<TInventoriesRouteNames> = {
|
|||||||
pagePath: () => '/inventories',
|
pagePath: () => '/inventories',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
transfer: {
|
||||||
|
path: 'inventories/transfer',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('../../views/transfer.component').then((m) => m.InventoriesTransferComponent),
|
||||||
|
meta: {
|
||||||
|
title: 'انتقال بین انبارها',
|
||||||
|
pagePath: () => '/inventories/transfer',
|
||||||
|
},
|
||||||
|
},
|
||||||
inventory: {
|
inventory: {
|
||||||
path: 'inventories/:inventoryId',
|
path: 'inventories/:inventoryId',
|
||||||
loadComponent: () => import('../../views/single.component').then((m) => m.InventoryComponent),
|
loadComponent: () => import('../../views/single.component').then((m) => m.InventoryComponent),
|
||||||
@@ -20,6 +35,24 @@ export const inventoriesNamedRoutes: NamedRoutes<TInventoriesRouteNames> = {
|
|||||||
pagePath: () => '/inventories/:inventoryId',
|
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);
|
export const INVENTORIES_ROUTES: Routes = Object.values(inventoriesNamedRoutes);
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from './io';
|
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;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
location: string;
|
location: string;
|
||||||
isActive: boolean;
|
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 {
|
export interface IInventoryRequest {
|
||||||
name: string;
|
name: string;
|
||||||
location: string;
|
location: string;
|
||||||
isActive: boolean;
|
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 { IPaginatedResponse } from '@/core/models/service.model';
|
||||||
|
import { MovementType } from '@/shared/catalog';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { INVENTORIES_API_ROUTES } from '../constants';
|
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' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class InventoriesService {
|
export class InventoriesService {
|
||||||
@@ -11,15 +26,50 @@ export class InventoriesService {
|
|||||||
|
|
||||||
private apiRoutes = INVENTORIES_API_ROUTES;
|
private apiRoutes = INVENTORIES_API_ROUTES;
|
||||||
|
|
||||||
getAll(): Observable<IPaginatedResponse<IInventoryResponse>> {
|
getAll(): Observable<IPaginatedResponse<IInventorySummaryResponse>> {
|
||||||
return this.http.get<IPaginatedResponse<IInventoryRawResponse>>(this.apiRoutes.list());
|
return this.http.get<IPaginatedResponse<IInventorySummaryRawResponse>>(this.apiRoutes.list());
|
||||||
}
|
}
|
||||||
|
|
||||||
getSingle(inventoryId: string): Observable<IInventoryResponse> {
|
getSingle(inventoryId: string): Observable<IInventoryDetailResponse> {
|
||||||
return this.http.get<IInventoryRawResponse>(this.apiRoutes.single(inventoryId));
|
return this.http.get<IInventoryDetailRawResponse>(this.apiRoutes.single(inventoryId));
|
||||||
}
|
}
|
||||||
|
|
||||||
create(data: IInventoryRequest): Observable<IInventoryResponse> {
|
create(data: IInventoryRequest): Observable<IInventorySummaryResponse> {
|
||||||
return this.http.post<IInventoryRawResponse>(this.apiRoutes.list(), data);
|
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()"
|
[items]="items()"
|
||||||
[loading]="loading()"
|
[loading]="loading()"
|
||||||
[showDetails]="true"
|
[showDetails]="true"
|
||||||
|
(onDetails)="toDetails($event)"
|
||||||
(onAdd)="openAddForm()"
|
(onAdd)="openAddForm()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import {
|
|||||||
IColumn,
|
IColumn,
|
||||||
PageDataListComponent,
|
PageDataListComponent,
|
||||||
} from '@/shared/components/pageDataList/page-data-list.component';
|
} 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 { InventoryFormComponent } from '../components/form.component';
|
||||||
import { IInventoryResponse } from '../models';
|
import { IInventorySummaryResponse } from '../models';
|
||||||
import { InventoriesService } from '../services/main.service';
|
import { InventoriesService } from '../services/main.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -13,6 +14,7 @@ import { InventoriesService } from '../services/main.service';
|
|||||||
imports: [PageDataListComponent, InventoryFormComponent],
|
imports: [PageDataListComponent, InventoryFormComponent],
|
||||||
})
|
})
|
||||||
export class InventoriesComponent {
|
export class InventoriesComponent {
|
||||||
|
private router = inject(Router);
|
||||||
constructor(private inventoryService: InventoriesService) {
|
constructor(private inventoryService: InventoriesService) {
|
||||||
this.getData();
|
this.getData();
|
||||||
}
|
}
|
||||||
@@ -21,11 +23,12 @@ export class InventoriesComponent {
|
|||||||
{ field: 'id', header: 'شناسه' },
|
{ field: 'id', header: 'شناسه' },
|
||||||
{ field: 'name', header: 'نام' },
|
{ field: 'name', header: 'نام' },
|
||||||
{ field: 'location', header: 'موقعیت' },
|
{ field: 'location', header: 'موقعیت' },
|
||||||
|
{ field: 'isPointOfSale', header: 'نقطه فروش' },
|
||||||
{ field: 'isActive', header: 'فعال' },
|
{ field: 'isActive', header: 'فعال' },
|
||||||
] as IColumn[];
|
] as IColumn[];
|
||||||
|
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
items = signal<IInventoryResponse[]>([]);
|
items = signal<IInventorySummaryResponse[]>([]);
|
||||||
visibleForm = signal(false);
|
visibleForm = signal(false);
|
||||||
|
|
||||||
refresh() {
|
refresh() {
|
||||||
@@ -43,4 +46,8 @@ export class InventoriesComponent {
|
|||||||
openAddForm() {
|
openAddForm() {
|
||||||
this.visibleForm.set(true);
|
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 { Maybe } from '@/core';
|
||||||
import { ProductChargeFormComponent } from '@/modules/productCharges/components/form.component';
|
|
||||||
import { KeyValueComponent } from '@/shared/components';
|
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 { Component, inject, signal } from '@angular/core';
|
||||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||||
import { Button, ButtonDirective } from 'primeng/button';
|
import { Button, ButtonDirective } from 'primeng/button';
|
||||||
import { Card } from 'primeng/card';
|
import { Card } from 'primeng/card';
|
||||||
import { GalleriaModule } from 'primeng/galleria';
|
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';
|
import { InventoriesService } from '../services/main.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -17,25 +20,28 @@ import { InventoriesService } from '../services/main.service';
|
|||||||
RouterLink,
|
RouterLink,
|
||||||
GalleriaModule,
|
GalleriaModule,
|
||||||
KeyValueComponent,
|
KeyValueComponent,
|
||||||
Card,
|
|
||||||
Button,
|
Button,
|
||||||
ProductChargeFormComponent,
|
InventoryMovementListComponent,
|
||||||
|
DetailsInfoCardComponent,
|
||||||
|
Card,
|
||||||
|
TableModule,
|
||||||
|
PriceMaskDirective,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class InventoryComponent {
|
export class InventoryComponent {
|
||||||
private route = inject(ActivatedRoute);
|
private route = inject(ActivatedRoute);
|
||||||
constructor(private service: InventoriesService) {
|
constructor(private service: InventoriesService) {
|
||||||
this.getData();
|
this.getData();
|
||||||
|
this.getStock();
|
||||||
}
|
}
|
||||||
|
|
||||||
productId = this.route.snapshot.params['productId'];
|
inventoryId = this.route.snapshot.params['inventoryId'];
|
||||||
isOpenChargeForm = signal(false);
|
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
data = signal<Maybe<IInventoryResponse>>(null);
|
data = signal<Maybe<IInventoryDetailResponse>>(null);
|
||||||
|
|
||||||
getData() {
|
getData() {
|
||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
this.service.getSingle(this.productId).subscribe({
|
this.service.getSingle(this.inventoryId).subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
this.data.set(res);
|
this.data.set(res);
|
||||||
this.loading.set(false);
|
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() {
|
get topBarCardDetails() {
|
||||||
return [
|
return [
|
||||||
// {
|
{
|
||||||
// icon: 'pi pi-box',
|
icon: 'pi pi-box',
|
||||||
// label: 'موجودی',
|
label: 'تعداد کالاهای موجود',
|
||||||
// value: this.data()? || 0,
|
value: this.data()?.availableProductCount,
|
||||||
// },
|
},
|
||||||
// {
|
{
|
||||||
// icon: 'pi pi-dollar',
|
icon: 'pi pi-dollar',
|
||||||
// label: 'قیمت پایه',
|
label: 'تنوع کالاها',
|
||||||
// value: this.data()?.basePrice?.toString() || 'تعیین نشده',
|
value: this.data()?.availableProductTypes,
|
||||||
// },
|
},
|
||||||
// {
|
{
|
||||||
// icon: 'pi pi-calendar',
|
icon: 'pi pi-dollar',
|
||||||
// label: 'تاریخ ایجاد',
|
label: 'ارزش کلی کالاهای موجود',
|
||||||
// value: this.data()?.createdAt
|
value: this.data()?.availableProductsCost,
|
||||||
// ? new Date(this.data()!.createdAt!).toLocaleDateString()
|
},
|
||||||
// : '-',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// icon: 'pi pi-clock',
|
|
||||||
// label: 'تعداد فروش',
|
|
||||||
// value: 1200,
|
|
||||||
// },
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
openDeleteConfirm() {}
|
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;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
deletedAt?: Maybe<Date>;
|
deletedAt?: Maybe<Date>;
|
||||||
|
stock: number;
|
||||||
|
avgCost?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProductResponse extends IProductRawResponse {}
|
export interface IProductResponse extends IProductRawResponse {}
|
||||||
|
|||||||
@@ -30,17 +30,7 @@
|
|||||||
<div class="flex grow flex-col gap-4">
|
<div class="flex grow flex-col gap-4">
|
||||||
<div class="grid grid-cols-4 gap-4">
|
<div class="grid grid-cols-4 gap-4">
|
||||||
@for (item of topBarCardDetails; track $index) {
|
@for (item of topBarCardDetails; track $index) {
|
||||||
<p-card class="w-full border border-primary-700">
|
<details-info-card [icon]="item.icon" [label]="item.label" [value]="item.value.toString()" />
|
||||||
<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>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Maybe } from '@/core';
|
import { Maybe } from '@/core';
|
||||||
import { PurchaseFormComponent } from '@/modules/purchases/components/form.component';
|
import { PurchaseFormComponent } from '@/modules/purchases/components/form.component';
|
||||||
import { KeyValueComponent } from '@/shared/components';
|
import { KeyValueComponent } from '@/shared/components';
|
||||||
|
import { DetailsInfoCardComponent } from '@/shared/components/detailsInfoCard/details-info-card.component';
|
||||||
import { Component, inject, signal } from '@angular/core';
|
import { Component, inject, signal } from '@angular/core';
|
||||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||||
import { Button, ButtonDirective } from 'primeng/button';
|
import { Button, ButtonDirective } from 'primeng/button';
|
||||||
@@ -20,6 +21,7 @@ import { ProductsService } from '../services/main.service';
|
|||||||
Card,
|
Card,
|
||||||
Button,
|
Button,
|
||||||
PurchaseFormComponent,
|
PurchaseFormComponent,
|
||||||
|
DetailsInfoCardComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ProductComponent {
|
export class ProductComponent {
|
||||||
@@ -79,7 +81,7 @@ export class ProductComponent {
|
|||||||
{
|
{
|
||||||
icon: 'pi pi-box',
|
icon: 'pi pi-box',
|
||||||
label: 'موجودی',
|
label: 'موجودی',
|
||||||
value: this.product()?.count || 0,
|
value: this.product()?.stock || 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'pi pi-dollar',
|
icon: 'pi pi-dollar',
|
||||||
@@ -87,16 +89,14 @@ export class ProductComponent {
|
|||||||
value: this.product()?.basePrice?.toString() || 'تعیین نشده',
|
value: this.product()?.basePrice?.toString() || 'تعیین نشده',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'pi pi-calendar',
|
icon: 'pi pi-dollar',
|
||||||
label: 'تاریخ ایجاد',
|
label: 'میانگین قیمت',
|
||||||
value: this.product()?.createdAt
|
value: this.product()?.avgCost?.toString() || 'تعیین نشده',
|
||||||
? new Date(this.product()!.createdAt!).toLocaleDateString()
|
|
||||||
: '-',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'pi pi-clock',
|
icon: 'pi pi-clock',
|
||||||
label: 'تعداد فروش',
|
label: 'تعداد فروش',
|
||||||
value: 1200,
|
value: '-',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { InventoriesSelectComponent } from '@/modules/inventories/components/select/select.component';
|
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 { IProductResponse } from '@/modules/products/models';
|
||||||
import { SuppliersSelectComponent } from '@/modules/suppliers/components/select/select.component';
|
import { SuppliersSelectComponent } from '@/modules/suppliers/components/select/select.component';
|
||||||
import { ISupplierResponse } from '@/modules/suppliers/models';
|
import { ISupplierResponse } from '@/modules/suppliers/models';
|
||||||
@@ -30,7 +30,7 @@ import { ProductChargeRowFormComponent } from './product-charge-row-form.compone
|
|||||||
})
|
})
|
||||||
export class PurchaseFormComponent {
|
export class PurchaseFormComponent {
|
||||||
@Input() product?: IProductResponse;
|
@Input() product?: IProductResponse;
|
||||||
@Input() inventory?: IInventoryResponse;
|
@Input() inventory?: IInventorySummaryResponse;
|
||||||
@Input() supplier?: ISupplierResponse;
|
@Input() supplier?: ISupplierResponse;
|
||||||
|
|
||||||
private fb = inject(FormBuilder);
|
private fb = inject(FormBuilder);
|
||||||
|
|||||||
+5
-4
@@ -1,7 +1,7 @@
|
|||||||
import { Maybe } from '@/core';
|
import { Maybe } from '@/core';
|
||||||
import { ToastService } from '@/core/services/toast.service';
|
import { ToastService } from '@/core/services/toast.service';
|
||||||
import { InventoriesSelectComponent } from '@/modules/inventories/components/select/select.component';
|
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 { IProductResponse } from '@/modules/products/models';
|
||||||
import { SuppliersSelectComponent } from '@/modules/suppliers/components/select/select.component';
|
import { SuppliersSelectComponent } from '@/modules/suppliers/components/select/select.component';
|
||||||
import { ISupplierResponse } from '@/modules/suppliers/models';
|
import { ISupplierResponse } from '@/modules/suppliers/models';
|
||||||
@@ -44,7 +44,7 @@ export class PurchaseReceiptTemplateComponent {
|
|||||||
private fb = inject(FormBuilder);
|
private fb = inject(FormBuilder);
|
||||||
|
|
||||||
@Input() product?: IProductResponse;
|
@Input() product?: IProductResponse;
|
||||||
@Input() inventory?: IInventoryResponse;
|
@Input() inventory?: IInventorySummaryResponse;
|
||||||
@Input() supplier?: ISupplierResponse;
|
@Input() supplier?: ISupplierResponse;
|
||||||
|
|
||||||
columns = [
|
columns = [
|
||||||
@@ -105,7 +105,7 @@ export class PurchaseReceiptTemplateComponent {
|
|||||||
total: [300000, [Validators.required, Validators.min(0)]],
|
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]],
|
supplier: [{ id: 1 } as Maybe<ISupplierResponse>, [Validators.required]],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -179,7 +179,8 @@ export class PurchaseReceiptTemplateComponent {
|
|||||||
next: (res) => {
|
next: (res) => {
|
||||||
this.toastService.success({ text: 'ثبت سند خرید با موفقیت انجام شد' });
|
this.toastService.success({ text: 'ثبت سند خرید با موفقیت انجام شد' });
|
||||||
this.form.enable();
|
this.form.enable();
|
||||||
this.form.reset();
|
// this.form.reset();
|
||||||
|
this.form.controls.code.setValue(this.form.controls.code.value + '1');
|
||||||
this.formIsSubmitted.set(false);
|
this.formIsSubmitted.set(false);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from './gender';
|
export * from './gender';
|
||||||
|
export * from './movementTypes';
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './tag.component';
|
||||||
|
export * from './types';
|
||||||
|
export * from './utils';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<p-tag [severity]="color">
|
||||||
|
{{ text }}
|
||||||
|
</p-tag>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { Tag } from 'primeng/tag';
|
||||||
|
import { MovementReferenceType } from './types';
|
||||||
|
import { getMovementReferenceTypeColor, getMovementReferenceTypeText } from './utils';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'catalog-movement-reference-tag',
|
||||||
|
templateUrl: './tag.component.html',
|
||||||
|
imports: [Tag],
|
||||||
|
})
|
||||||
|
export class CatalogMovementReferenceTagComponent {
|
||||||
|
@Input() type!: MovementReferenceType;
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
get text() {
|
||||||
|
return getMovementReferenceTypeText(this.type);
|
||||||
|
}
|
||||||
|
get color() {
|
||||||
|
return getMovementReferenceTypeColor(this.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export const MovementReferenceType = {
|
||||||
|
PURCHASE: 'PURCHASE',
|
||||||
|
SALES: 'SALES',
|
||||||
|
ADJUSTMENT: 'ADJUSTMENT',
|
||||||
|
INVENTORY_TRANSFER: 'INVENTORY_TRANSFER',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type MovementReferenceType =
|
||||||
|
(typeof MovementReferenceType)[keyof typeof MovementReferenceType];
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { TagSeverity } from '@/core/models/tag';
|
||||||
|
import { MovementReferenceType } from './types';
|
||||||
|
|
||||||
|
export const getMovementReferenceTypeText = (movementType: MovementReferenceType): string => {
|
||||||
|
switch (movementType) {
|
||||||
|
case MovementReferenceType.PURCHASE:
|
||||||
|
return 'خرید';
|
||||||
|
case MovementReferenceType.SALES:
|
||||||
|
return 'فروش';
|
||||||
|
case MovementReferenceType.ADJUSTMENT:
|
||||||
|
return 'تعدیل';
|
||||||
|
case MovementReferenceType.INVENTORY_TRANSFER:
|
||||||
|
return 'انتقال بین انبارها';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMovementReferenceTypeColor = (movementType: MovementReferenceType): TagSeverity => {
|
||||||
|
switch (movementType) {
|
||||||
|
case MovementReferenceType.PURCHASE:
|
||||||
|
return 'success';
|
||||||
|
case MovementReferenceType.SALES:
|
||||||
|
return 'danger';
|
||||||
|
case MovementReferenceType.ADJUSTMENT:
|
||||||
|
return 'warn';
|
||||||
|
case MovementReferenceType.INVENTORY_TRANSFER:
|
||||||
|
return 'info';
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './tag.component';
|
||||||
|
export * from './types';
|
||||||
|
export * from './utils';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<p-tag [severity]="color">
|
||||||
|
{{ text }}
|
||||||
|
</p-tag>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { Tag } from 'primeng/tag';
|
||||||
|
import { MovementType } from './types';
|
||||||
|
import { getMovementTypeColor, getMovementTypeText } from './utils';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'catalog-movement-tag',
|
||||||
|
templateUrl: './tag.component.html',
|
||||||
|
imports: [Tag],
|
||||||
|
})
|
||||||
|
export class CatalogMovementTagComponent {
|
||||||
|
@Input() type!: MovementType;
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
get text() {
|
||||||
|
return getMovementTypeText(this.type);
|
||||||
|
}
|
||||||
|
get color() {
|
||||||
|
return getMovementTypeColor(this.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export const MovementType = {
|
||||||
|
IN: 'IN',
|
||||||
|
OUT: 'OUT',
|
||||||
|
ADJUST: 'ADJUST',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type MovementType = (typeof MovementType)[keyof typeof MovementType];
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { TagSeverity } from '@/core/models/tag';
|
||||||
|
import { MovementType } from './types';
|
||||||
|
|
||||||
|
export const getMovementTypeText = (movementType: MovementType): string => {
|
||||||
|
switch (movementType) {
|
||||||
|
case MovementType.IN:
|
||||||
|
return 'ورودی';
|
||||||
|
case MovementType.OUT:
|
||||||
|
return 'خروجی';
|
||||||
|
case MovementType.ADJUST:
|
||||||
|
return 'تعدیل';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMovementTypeColor = (movementType: MovementType): TagSeverity => {
|
||||||
|
switch (movementType) {
|
||||||
|
case MovementType.IN:
|
||||||
|
return 'success';
|
||||||
|
case MovementType.OUT:
|
||||||
|
return 'danger';
|
||||||
|
case MovementType.ADJUST:
|
||||||
|
return 'warn';
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<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="3" class="text-center!" [style]="{ width: '25rem' }">مانده</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" /></td>
|
||||||
|
<td class="text-center!">{{ item.referenceId || "-" }}</td>
|
||||||
|
<td class="text-center!">{{ item.supplier?.name || "-" }}</td>
|
||||||
|
<td class="text-center!">{{ item.inventory.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 { ICardexItem } from './type';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'shared-cardex',
|
||||||
|
templateUrl: './cardex.component.html',
|
||||||
|
imports: [
|
||||||
|
TableModule,
|
||||||
|
JalaliDateDirective,
|
||||||
|
CatalogMovementReferenceTagComponent,
|
||||||
|
PriceMaskDirective,
|
||||||
|
],
|
||||||
|
hostDirectives: [PriceMaskDirective],
|
||||||
|
})
|
||||||
|
export class CardexComponent {
|
||||||
|
@Input() items: ICardexItem[] = [];
|
||||||
|
@Input() loading: boolean = false;
|
||||||
|
constructor() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { IInventoryStockMovementResponse } from '@/modules/inventories/models';
|
||||||
|
|
||||||
|
export interface ICardexItem extends IInventoryStockMovementResponse {}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<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 ' + icon + ' text-2xl'"></i>
|
||||||
|
</div>
|
||||||
|
<div class="grow flex flex-col gap-1">
|
||||||
|
<span class="text-sm">{{ label }}</span>
|
||||||
|
<span class="text-xl font-bold text-primary">{{ value }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p-card>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { Card } from 'primeng/card';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'details-info-card',
|
||||||
|
templateUrl: './details-info-card.component.html',
|
||||||
|
imports: [Card],
|
||||||
|
})
|
||||||
|
export class DetailsInfoCardComponent {
|
||||||
|
@Input() icon: string = '';
|
||||||
|
@Input() label: string = '';
|
||||||
|
@Input() value: string = '';
|
||||||
|
constructor() {}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PaginatorComponent, UikitCopyComponent, UikitEmptyStateComponent } from '@/uikit';
|
import { PaginatorComponent, UikitCopyComponent, UikitEmptyStateComponent } from '@/uikit';
|
||||||
|
import { formatJalali } from '@/utils';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
@@ -124,8 +125,7 @@ export class PageDataListComponent<I> {
|
|||||||
switch (column.type) {
|
switch (column.type) {
|
||||||
case 'date':
|
case 'date':
|
||||||
if (!data) return '-';
|
if (!data) return '-';
|
||||||
const date = new Date(data);
|
return formatJalali(data);
|
||||||
return isNaN(date.getTime()) ? '-' : date.toLocaleDateString();
|
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
return data ? 'بله' : 'خیر';
|
return data ? 'بله' : 'خیر';
|
||||||
case 'price':
|
case 'price':
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
export * from './price-alphabet.directive';
|
export * from './price-alphabet.directive';
|
||||||
export * from './price-mask.directive';
|
export * from './price-mask.directive';
|
||||||
|
|
||||||
|
export * from './jalali-date.directive';
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { formatJalali } from '@/utils';
|
||||||
|
import { Directive, ElementRef, Input, OnChanges, Renderer2, SimpleChanges } from '@angular/core';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[jalaliDate]',
|
||||||
|
})
|
||||||
|
export class JalaliDateDirective implements OnChanges {
|
||||||
|
@Input('jalaliDate') dateValue!: string | number | Date;
|
||||||
|
@Input() jalaliFormat: string = 'YYYY/MM/DD';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private el: ElementRef,
|
||||||
|
private renderer: Renderer2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (this.dateValue) {
|
||||||
|
const formatted = formatJalali(this.dateValue, this.jalaliFormat);
|
||||||
|
this.renderer.setProperty(this.el.nativeElement, 'innerText', formatted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ export * from './copy/copy.component';
|
|||||||
export * from './datepicker/datepicker.component';
|
export * from './datepicker/datepicker.component';
|
||||||
export * from './duration-picker/duration-picker.component';
|
export * from './duration-picker/duration-picker.component';
|
||||||
export * from './paginator.component';
|
export * from './paginator.component';
|
||||||
|
export * from './uikit-counter.component';
|
||||||
export * from './uikit-emptystate.component';
|
export * from './uikit-emptystate.component';
|
||||||
export * from './uikit-field.component';
|
export * from './uikit-field.component';
|
||||||
export * from './uikit-label.component';
|
export * from './uikit-label.component';
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<div class="flex items-center border rounded overflow-hidden bg-white" [class.opacity-60]="disabled">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
pButton
|
||||||
|
class="p-button p-component flex items-center justify-center px-3"
|
||||||
|
(click)="decrease()"
|
||||||
|
[disabled]="disabled"
|
||||||
|
aria-label="decrease"
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="text-center w-16 px-2 py-1 outline-none"
|
||||||
|
type="number"
|
||||||
|
[value]="value"
|
||||||
|
(input)="onInput($event)"
|
||||||
|
[min]="min ?? undefined"
|
||||||
|
[max]="max ?? undefined"
|
||||||
|
[step]="step"
|
||||||
|
[disabled]="disabled"
|
||||||
|
aria-label="quantity"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
pButton
|
||||||
|
class="p-button p-component flex items-center justify-center px-3"
|
||||||
|
(click)="increase()"
|
||||||
|
[disabled]="disabled"
|
||||||
|
aria-label="increase"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tailwind handles the main layout. Keep a couple of small overrides for cross-browser input appearance. */
|
||||||
|
.uikit-counter__input {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uikit-counter__input::-webkit-outer-spin-button,
|
||||||
|
.uikit-counter__input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { ButtonModule } from 'primeng/button';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-uikit-counter',
|
||||||
|
templateUrl: './uikit-counter.component.html',
|
||||||
|
styleUrls: ['./uikit-counter.component.scss'],
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, ButtonModule],
|
||||||
|
})
|
||||||
|
export class UikitCounterComponent {
|
||||||
|
@Input() value = 0;
|
||||||
|
@Input() min: number | null = null;
|
||||||
|
@Input() max: number | null = null;
|
||||||
|
@Input() step = 1;
|
||||||
|
@Input() disabled = false;
|
||||||
|
|
||||||
|
@Output() valueChange = new EventEmitter<number>();
|
||||||
|
|
||||||
|
clamp(v: number): number {
|
||||||
|
if (this.min !== null && v < this.min) return this.min;
|
||||||
|
if (this.max !== null && v > this.max) return this.max;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(v: number) {
|
||||||
|
const clamped = this.clamp(v);
|
||||||
|
if (clamped !== this.value) {
|
||||||
|
this.value = clamped;
|
||||||
|
this.valueChange.emit(this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
increase() {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this.setValue(this.value + this.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
decrease() {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this.setValue(this.value - this.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
onInput(e: Event) {
|
||||||
|
const v = (e.target as HTMLInputElement).value;
|
||||||
|
const n = Number(v);
|
||||||
|
if (!Number.isNaN(n)) {
|
||||||
|
this.setValue(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './currency';
|
export * from './currency';
|
||||||
|
export * from './jalali-date.utils';
|
||||||
export * from './name';
|
export * from './name';
|
||||||
export * from './time';
|
export * from './time';
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
|
import jalaliday from 'jalaliday';
|
||||||
|
|
||||||
|
dayjs.extend(jalaliday);
|
||||||
|
|
||||||
|
export function toJalali(date: string | number | Date | Dayjs, format = 'YYYY/MM/DD'): Dayjs {
|
||||||
|
return dayjs(date).calendar('jalali');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromJalali(jalaliDate: string): Dayjs {
|
||||||
|
// Parse Jalali date string (e.g., '1404-09-21')
|
||||||
|
return dayjs(jalaliDate, { jalali: true }).calendar('gregory');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatJalali(date: string | number | Date | Dayjs, format = 'YYYY/MM/DD'): string {
|
||||||
|
return toJalali(date).format(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function jalaliDiff(
|
||||||
|
date1: string | number | Date | Dayjs,
|
||||||
|
date2: string | number | Date | Dayjs,
|
||||||
|
unit: dayjs.OpUnitType = 'day',
|
||||||
|
): number {
|
||||||
|
return toJalali(date1).diff(toJalali(date2), unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isJalaliBefore(
|
||||||
|
date1: string | number | Date | Dayjs,
|
||||||
|
date2: string | number | Date | Dayjs,
|
||||||
|
): boolean {
|
||||||
|
return toJalali(date1).isBefore(toJalali(date2));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isJalaliAfter(
|
||||||
|
date1: string | number | Date | Dayjs,
|
||||||
|
date2: string | number | Date | Dayjs,
|
||||||
|
): boolean {
|
||||||
|
return toJalali(date1).isAfter(toJalali(date2));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nowJalali(): Dayjs {
|
||||||
|
return dayjs().calendar('jalali');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user