feat: enhance product and purchase components
- Added new components for handling purchase receipts and payment wrappers. - Updated product details view to include sales count and stock alerts. - Refactored invoice payment form to use a template for better structure. - Introduced confirmation dialog service for payment confirmations. - Improved state card component for better visual representation of orders. - Added loading indicators and error handling in various components. - Updated routes to include new purchase functionality for suppliers. - Enhanced stock alert component to visually indicate low stock levels.
This commit is contained in:
@@ -1,14 +1,16 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { ConfirmDialog } from 'primeng/confirmdialog';
|
||||||
import { ToastModule } from 'primeng/toast';
|
import { ToastModule } from 'primeng/toast';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterModule, ToastModule],
|
imports: [RouterModule, ToastModule, ConfirmDialog],
|
||||||
template: `
|
template: `
|
||||||
<div>
|
<div>
|
||||||
<p-toast position="bottom-right" />
|
<p-toast position="bottom-right" />
|
||||||
|
<p-confirmDialog />
|
||||||
<router-outlet />
|
<router-outlet />
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|||||||
+2
-1
@@ -20,7 +20,7 @@ import {
|
|||||||
withInMemoryScrolling,
|
withInMemoryScrolling,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
// Use the consolidated preset that includes our custom variables
|
// Use the consolidated preset that includes our custom variables
|
||||||
import { MessageService } from 'primeng/api';
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
import { providePrimeNG } from 'primeng/config';
|
import { providePrimeNG } from 'primeng/config';
|
||||||
import { appRoutes } from './app.routes';
|
import { appRoutes } from './app.routes';
|
||||||
import MyPreset from './presets';
|
import MyPreset from './presets';
|
||||||
@@ -66,6 +66,7 @@ export const appConfig: ApplicationConfig = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
MessageService,
|
MessageService,
|
||||||
|
ConfirmationService,
|
||||||
|
|
||||||
provideHttpClient(
|
provideHttpClient(
|
||||||
withFetch(),
|
withFetch(),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const INVENTORIES_API_ROUTES = {
|
|||||||
inventoryMovements: (inventoryId: string) => `${baseUrl}/${inventoryId}/movements`,
|
inventoryMovements: (inventoryId: string) => `${baseUrl}/${inventoryId}/movements`,
|
||||||
transfer: () => `${baseUrl}/transfers`,
|
transfer: () => `${baseUrl}/transfers`,
|
||||||
stock: (inventoryId: string) => `${baseUrl}/${inventoryId}/stock`,
|
stock: (inventoryId: string) => `${baseUrl}/${inventoryId}/stock`,
|
||||||
|
cardex: (inventoryId: string) => `${baseUrl}/${inventoryId}/cardex`,
|
||||||
productCardex: (inventoryId: string, productId: string) =>
|
productCardex: (inventoryId: string, productId: string) =>
|
||||||
`${baseUrl}/${inventoryId}/products/${productId}/cardex`,
|
`${baseUrl}/${inventoryId}/products/${productId}/cardex`,
|
||||||
bankAccounts: (inventoryId: number) => `${baseUrl}/${inventoryId}/bank-accounts`,
|
bankAccounts: (inventoryId: number) => `${baseUrl}/${inventoryId}/bank-accounts`,
|
||||||
|
|||||||
@@ -37,12 +37,21 @@ export const inventoriesNamedRoutes: NamedRoutes<any> = {
|
|||||||
pagePath: () => '/inventories/:inventoryId/products',
|
pagePath: () => '/inventories/:inventoryId/products',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
cardex: {
|
||||||
|
path: 'inventories/:inventoryId/cardex',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('../../views/cardex.component').then((m) => m.InventoryCardexComponent),
|
||||||
|
meta: {
|
||||||
|
title: 'کاردکس انبار',
|
||||||
|
pagePath: () => '/inventories/:inventoryId/cardex',
|
||||||
|
},
|
||||||
|
},
|
||||||
productCardex: {
|
productCardex: {
|
||||||
path: 'inventories/:inventoryId/products/:productId/cardex',
|
path: 'inventories/:inventoryId/products/:productId/cardex',
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('../../views/product-cardex.component').then((m) => m.InventoryProductCardexComponent),
|
import('../../views/product-cardex.component').then((m) => m.InventoryProductCardexComponent),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'کارتکس محصول',
|
title: 'کاردکس انبار محصول',
|
||||||
pagePath: () => '/inventories/:inventoryId/products/:productId/cardex',
|
pagePath: () => '/inventories/:inventoryId/products/:productId/cardex',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import { ICardexResponse } from '@/modules/cardex/models';
|
||||||
import {
|
import {
|
||||||
IInventoryInfo,
|
IInventoryInfo,
|
||||||
IInventoryMovement,
|
IInventoryMovement,
|
||||||
@@ -61,6 +62,6 @@ export interface IInventoryStockQuery {
|
|||||||
isAvailable?: boolean;
|
isAvailable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IInventoryProductCardexRawResponse extends IInventoryStockMovementRawResponse {}
|
export interface IInventoryProductCardexRawResponse extends ICardexResponse {}
|
||||||
|
|
||||||
export interface IInventoryProductCardexResponse extends IInventoryProductCardexRawResponse {}
|
export interface IInventoryProductCardexResponse extends IInventoryProductCardexRawResponse {}
|
||||||
|
|||||||
@@ -72,4 +72,9 @@ export class InventoriesService {
|
|||||||
`${this.apiRoutes.productCardex(inventoryId, productId)}`,
|
`${this.apiRoutes.productCardex(inventoryId, productId)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
getCardex(inventoryId: string): Observable<IPaginatedResponse<IInventoryProductCardexResponse>> {
|
||||||
|
return this.http.get<IPaginatedResponse<IInventoryProductCardexRawResponse>>(
|
||||||
|
`${this.apiRoutes.cardex(inventoryId)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
@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">کاردکس انبار {{ inventory()?.name }}</span>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<cardex-templates-inventory [loading]="getCardexLoading()" [items]="cardex() || []" variant="inventory" />
|
||||||
|
</p-card>
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Maybe } from '@/core';
|
||||||
|
import { CardexComponent } from '@/modules/cardex/components/templates/inventory.component';
|
||||||
|
import { IProductResponse } from '@/modules/products/models';
|
||||||
|
import { Component, computed, inject, signal } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { Card } from 'primeng/card';
|
||||||
|
import { ProgressSpinner } from 'primeng/progressspinner';
|
||||||
|
import { IInventoryProductCardexResponse } from '../models';
|
||||||
|
import { InventoriesService } from '../services/main.service';
|
||||||
|
import { InventoryStore } from '../store/inventory.store';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'inventory-cardex',
|
||||||
|
templateUrl: './cardex.component.html',
|
||||||
|
imports: [ProgressSpinner, Card, CardexComponent],
|
||||||
|
})
|
||||||
|
export class InventoryCardexComponent {
|
||||||
|
private route = inject(ActivatedRoute);
|
||||||
|
private readonly store = inject(InventoryStore);
|
||||||
|
|
||||||
|
inventoryId = this.route.snapshot.paramMap.get('inventoryId')!;
|
||||||
|
|
||||||
|
inventory = computed(() => this.store.entities()[this.inventoryId]);
|
||||||
|
loading = this.store.loading;
|
||||||
|
|
||||||
|
getInventoryLoading = signal(false);
|
||||||
|
getProductLoading = signal(false);
|
||||||
|
product = signal<Maybe<IProductResponse>>(null);
|
||||||
|
|
||||||
|
getCardexLoading = signal(false);
|
||||||
|
cardex = signal<Maybe<IInventoryProductCardexResponse[]>>(null);
|
||||||
|
|
||||||
|
get pageLoading() {
|
||||||
|
return this.getInventoryLoading() || this.getProductLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private service: InventoriesService) {
|
||||||
|
this.store.getSingle(this.inventoryId);
|
||||||
|
this.getCardex();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCardex() {
|
||||||
|
this.service.getCardex(this.inventoryId).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.getCardexLoading.set(false);
|
||||||
|
this.cardex.set(res.data);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.getCardexLoading.set(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,12 +12,29 @@
|
|||||||
[columns]="columns"
|
[columns]="columns"
|
||||||
emptyPlaceholderTitle="نقطه فروش برای این انبار تعریف نشده است"
|
emptyPlaceholderTitle="نقطه فروش برای این انبار تعریف نشده است"
|
||||||
emptyPlaceholderDescription="برای تعریف نقطهی فروش، روی دکمهٔ بالا کلیک کنید."
|
emptyPlaceholderDescription="برای تعریف نقطهی فروش، روی دکمهٔ بالا کلیک کنید."
|
||||||
[showDetails]="true"
|
|
||||||
[items]="items()"
|
[items]="items()"
|
||||||
[loading]="loading()"
|
[loading]="loading()"
|
||||||
(onAdd)="openAddForm()"
|
(onAdd)="openAddForm()"
|
||||||
(onDetails)="toPos($event)"
|
(onDetails)="toPos($event)"
|
||||||
/>
|
>
|
||||||
|
<ng-template #salesInfoTpl let-item>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
@if (item.todaySalesInfo.salesCount) {
|
||||||
|
<span [appPriceMask]="item.todaySalesInfo.salesTotal"></span>
|
||||||
|
<span class="block text-muted-color text-sm">{{ item.todaySalesInfo.salesCount }} فاکتور</span>
|
||||||
|
} @else {
|
||||||
|
<span class="text-muted-color">فروشی ثبت نشده است</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #actionsTpl let-item>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<p-button size="small" icon="pi pi-info" (click)="toPosInfo(item)" title="جزئیات" />
|
||||||
|
<p-button size="small" icon="pi pi-chevron-left" (click)="toPos(item)" title="ورود به صفحهی فروش" />
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</app-page-data-list>
|
||||||
@if (inventoryId) {
|
@if (inventoryId) {
|
||||||
<app-inventory-pos-account-form [(visible)]="visibleForm" [inventoryId]="inventoryId" />
|
<app-inventory-pos-account-form [(visible)]="visibleForm" [inventoryId]="inventoryId" />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import {
|
|||||||
IColumn,
|
IColumn,
|
||||||
PageDataListComponent,
|
PageDataListComponent,
|
||||||
} from '@/shared/components/pageDataList/page-data-list.component';
|
} from '@/shared/components/pageDataList/page-data-list.component';
|
||||||
import { Component, computed, inject, signal } from '@angular/core';
|
import { PriceMaskDirective } from '@/shared/directives';
|
||||||
|
import { Component, computed, inject, signal, TemplateRef, ViewChild } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { ButtonDirective } from 'primeng/button';
|
import { Button, ButtonDirective } from 'primeng/button';
|
||||||
import { Card } from 'primeng/card';
|
import { Card } from 'primeng/card';
|
||||||
import { InventoryPosAccountFormComponent } from '../../components/posAccounts/form.component';
|
import { InventoryPosAccountFormComponent } from '../../components/posAccounts/form.component';
|
||||||
import { IInventoryPosAccountResponse } from '../../models';
|
import { IInventoryPosAccountResponse } from '../../models';
|
||||||
@@ -21,6 +22,8 @@ import { InventoryStore } from '../../store/inventory.store';
|
|||||||
ButtonDirective,
|
ButtonDirective,
|
||||||
InventoryPosAccountFormComponent,
|
InventoryPosAccountFormComponent,
|
||||||
InnerPagesHeaderComponent,
|
InnerPagesHeaderComponent,
|
||||||
|
PriceMaskDirective,
|
||||||
|
Button,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class InventoryPosAccountListComponent {
|
export class InventoryPosAccountListComponent {
|
||||||
@@ -34,9 +37,13 @@ export class InventoryPosAccountListComponent {
|
|||||||
visibleForm = signal<boolean>(false);
|
visibleForm = signal<boolean>(false);
|
||||||
inventory = computed(() => this.inventoryStore.entities()[this.inventoryId]);
|
inventory = computed(() => this.inventoryStore.entities()[this.inventoryId]);
|
||||||
|
|
||||||
|
@ViewChild('salesInfoTpl', { static: true }) salesInfoTpl!: TemplateRef<any>;
|
||||||
|
@ViewChild('actionsTpl', { static: true }) actionsTpl!: TemplateRef<any>;
|
||||||
|
|
||||||
columns = [] as IColumn<IInventoryPosAccountResponse>[];
|
columns = [] as IColumn<IInventoryPosAccountResponse>[];
|
||||||
|
|
||||||
pageTitle = computed(() => `نقاط فروش متصل به انبار ${this.inventory()?.name || ''}`);
|
pageTitle = computed(() => `نقاط فروش متصل به انبار ${this.inventory()?.name || ''}`);
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.inventoryStore.getSingle(this.inventoryId);
|
this.inventoryStore.getSingle(this.inventoryId);
|
||||||
this.getItems();
|
this.getItems();
|
||||||
@@ -55,13 +62,18 @@ export class InventoryPosAccountListComponent {
|
|||||||
return `${item.bankAccount.name} - بانک ${item.bankAccount.branch.bank.name} - شعبهی ${item.bankAccount.branch.name}`;
|
return `${item.bankAccount.name} - بانک ${item.bankAccount.branch.bank.name} - شعبهی ${item.bankAccount.branch.name}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'salesInfo',
|
||||||
|
header: 'جزییات فروش امروز',
|
||||||
|
customDataModel: this.salesInfoTpl,
|
||||||
|
},
|
||||||
|
|
||||||
// {
|
{
|
||||||
// field: 'actions',
|
field: 'actions',
|
||||||
// header: '',
|
header: '',
|
||||||
// customDataModel: this.actionsTpl,
|
customDataModel: this.actionsTpl,
|
||||||
// width: '100px',
|
width: '100px',
|
||||||
// },
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,4 +101,6 @@ export class InventoryPosAccountListComponent {
|
|||||||
toPos(item: IInventoryPosAccountResponse) {
|
toPos(item: IInventoryPosAccountResponse) {
|
||||||
window.open(`/pos/${item.id}`, '_blank');
|
window.open(`/pos/${item.id}`, '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toPosInfo(item: IInventoryPosAccountResponse) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<shared-cardex [loading]="getCardexLoading()" [items]="cardex() || []" />
|
<cardex-templates-inventory [loading]="getCardexLoading()" [items]="cardex() || []" variant="general" />
|
||||||
</p-card>
|
</p-card>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Maybe } from '@/core';
|
import { Maybe } from '@/core';
|
||||||
|
import { CardexComponent } from '@/modules/cardex/components/templates/inventory.component';
|
||||||
import { IProductResponse } from '@/modules/products/models';
|
import { IProductResponse } from '@/modules/products/models';
|
||||||
import { ProductsService } from '@/modules/products/services/main.service';
|
import { ProductsService } from '@/modules/products/services/main.service';
|
||||||
import { CardexComponent } from '@/shared/components/cardex/cardex.component';
|
import { Component, computed, inject, signal } from '@angular/core';
|
||||||
import { Component, inject, signal } from '@angular/core';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { Card } from 'primeng/card';
|
import { Card } from 'primeng/card';
|
||||||
import { ProgressSpinner } from 'primeng/progressspinner';
|
import { ProgressSpinner } from 'primeng/progressspinner';
|
||||||
import { IInventoryDetailResponse, IInventoryProductCardexResponse } from '../models';
|
import { IInventoryProductCardexResponse } from '../models';
|
||||||
import { InventoriesService } from '../services/main.service';
|
import { InventoriesService } from '../services/main.service';
|
||||||
|
import { InventoryStore } from '../store/inventory.store';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'inventory-product-cardex',
|
selector: 'inventory-product-cardex',
|
||||||
@@ -16,20 +17,15 @@ import { InventoriesService } from '../services/main.service';
|
|||||||
})
|
})
|
||||||
export class InventoryProductCardexComponent {
|
export class InventoryProductCardexComponent {
|
||||||
private route = inject(ActivatedRoute);
|
private route = inject(ActivatedRoute);
|
||||||
constructor(
|
private readonly store = inject(InventoryStore);
|
||||||
private service: InventoriesService,
|
|
||||||
private productService: ProductsService,
|
|
||||||
) {
|
|
||||||
this.getInventory();
|
|
||||||
this.getProduct();
|
|
||||||
this.getCardex();
|
|
||||||
}
|
|
||||||
|
|
||||||
inventoryId = this.route.snapshot.params['inventoryId'];
|
inventoryId = this.route.snapshot.paramMap.get('inventoryId')!;
|
||||||
productId = this.route.snapshot.params['productId'];
|
productId = this.route.snapshot.params['productId'];
|
||||||
|
|
||||||
|
inventory = computed(() => this.store.entities()[this.inventoryId]);
|
||||||
|
loading = this.store.loading;
|
||||||
|
|
||||||
getInventoryLoading = signal(false);
|
getInventoryLoading = signal(false);
|
||||||
inventory = signal<Maybe<IInventoryDetailResponse>>(null);
|
|
||||||
getProductLoading = signal(false);
|
getProductLoading = signal(false);
|
||||||
product = signal<Maybe<IProductResponse>>(null);
|
product = signal<Maybe<IProductResponse>>(null);
|
||||||
|
|
||||||
@@ -40,17 +36,13 @@ export class InventoryProductCardexComponent {
|
|||||||
return this.getInventoryLoading() || this.getProductLoading();
|
return this.getInventoryLoading() || this.getProductLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
getInventory() {
|
constructor(
|
||||||
this.getInventoryLoading.set(true);
|
private service: InventoriesService,
|
||||||
this.service.getSingle(this.inventoryId).subscribe({
|
private productService: ProductsService,
|
||||||
next: (res) => {
|
) {
|
||||||
this.inventory.set(res);
|
this.store.getSingle(this.inventoryId);
|
||||||
this.getInventoryLoading.set(false);
|
this.getProduct();
|
||||||
},
|
this.getCardex();
|
||||||
error: () => {
|
|
||||||
this.getInventoryLoading.set(false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getProduct() {
|
getProduct() {
|
||||||
|
|||||||
@@ -46,6 +46,11 @@
|
|||||||
</p-card>
|
</p-card>
|
||||||
<p-card header="اطلاعات تکمیلی">
|
<p-card header="اطلاعات تکمیلی">
|
||||||
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
|
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
|
||||||
|
<app-input
|
||||||
|
label="حداقل سطح هشدار موجودی"
|
||||||
|
[control]="form.controls.minimumStockAlertLevel"
|
||||||
|
name="minimumStockAlertLevel"
|
||||||
|
/>
|
||||||
<product-categories-select-field [control]="form.controls.categoryId" [canInsert]="true" />
|
<product-categories-select-field [control]="form.controls.categoryId" [canInsert]="true" />
|
||||||
</form>
|
</form>
|
||||||
</p-card>
|
</p-card>
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ export class ProductFullFormComponent {
|
|||||||
barcode: [this.initialData?.barcode || null],
|
barcode: [this.initialData?.barcode || null],
|
||||||
description: [this.initialData?.description || null],
|
description: [this.initialData?.description || null],
|
||||||
categoryId: [this.initialData?.category?.id || null],
|
categoryId: [this.initialData?.category?.id || null],
|
||||||
|
minimumStockAlertLevel: [
|
||||||
|
this.initialData?.minimumStockAlertLevel || 1,
|
||||||
|
[Validators.required, Validators.min(0)],
|
||||||
|
],
|
||||||
imageUrl: [null],
|
imageUrl: [null],
|
||||||
salePrice: [this.initialData?.salePrice || 0, [Validators.required, Validators.min(0)]],
|
salePrice: [this.initialData?.salePrice || 0, [Validators.required, Validators.min(0)]],
|
||||||
});
|
});
|
||||||
@@ -66,7 +70,7 @@ export class ProductFullFormComponent {
|
|||||||
submit() {
|
submit() {
|
||||||
this.form.markAsTouched();
|
this.form.markAsTouched();
|
||||||
if (this.form.valid) {
|
if (this.form.valid) {
|
||||||
this.onSubmit.emit(this.form.value as unknown as IProductRequest);
|
this.onSubmit.emit(this.form.value as IProductRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-6
@@ -1,6 +1,7 @@
|
|||||||
import { Maybe } from '@/core';
|
import { Maybe } from '@/core';
|
||||||
import { IPaginatedQuery } from '@/core/models/service.model';
|
import { IPaginatedQuery } from '@/core/models/service.model';
|
||||||
import { IProductBrand, IProductCategory } from './others';
|
import ISummary from '@/core/models/summary';
|
||||||
|
import { IProductStockBalanceSummary } from './types';
|
||||||
|
|
||||||
export interface IGetProductsQueryParams extends IPaginatedQuery {}
|
export interface IGetProductsQueryParams extends IPaginatedQuery {}
|
||||||
|
|
||||||
@@ -10,17 +11,17 @@ export interface IProductRawResponse {
|
|||||||
description?: string;
|
description?: string;
|
||||||
barcode?: Maybe<string>;
|
barcode?: Maybe<string>;
|
||||||
sku: string;
|
sku: string;
|
||||||
// productType: string;
|
brand?: ISummary;
|
||||||
brand?: IProductBrand;
|
category?: ISummary;
|
||||||
category?: IProductCategory;
|
|
||||||
// supplierId: number;
|
|
||||||
salePrice?: string;
|
salePrice?: string;
|
||||||
count?: number;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
deletedAt?: Maybe<Date>;
|
deletedAt?: Maybe<Date>;
|
||||||
stock: number;
|
stock: number;
|
||||||
avgCost?: number;
|
avgCost?: number;
|
||||||
|
stockBalances: IProductStockBalanceSummary[];
|
||||||
|
salesCount: number;
|
||||||
|
minimumStockAlertLevel: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProductResponse extends IProductRawResponse {}
|
export interface IProductResponse extends IProductRawResponse {}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
export interface IProductCategory {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
export interface IProductBrand {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import ISummary from '@/core/models/summary';
|
||||||
|
|
||||||
|
export interface IProductCategory {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
export interface IProductBrand {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProductStockBalanceSummary {
|
||||||
|
avgCost?: number;
|
||||||
|
quantity: number;
|
||||||
|
inventory: ISummary;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<app-page-data-list
|
<app-page-data-list
|
||||||
[pageTitle]="'مدیریت کالاها'"
|
[pageTitle]="'مدیریت کالاها'"
|
||||||
[addNewCtaLabel]="'افزودن کالا جدید'"
|
[addNewCtaLabel]="'افزودن کالا جدید'"
|
||||||
[columns]="columns"
|
[columns]="columns()"
|
||||||
[showAdd]="true"
|
[showAdd]="true"
|
||||||
emptyPlaceholderTitle="کالای یافت نشد"
|
emptyPlaceholderTitle="کالای یافت نشد"
|
||||||
emptyPlaceholderDescription="برای افزودن کالا جدید، روی دکمهٔ بالا کلیک کنید."
|
emptyPlaceholderDescription="برای افزودن کالا جدید، روی دکمهٔ بالا کلیک کنید."
|
||||||
@@ -14,6 +14,10 @@
|
|||||||
(onAdd)="openAddForm()"
|
(onAdd)="openAddForm()"
|
||||||
(onDetails)="toDetails($event)"
|
(onDetails)="toDetails($event)"
|
||||||
(pageChange)="onPageChange($event)"
|
(pageChange)="onPageChange($event)"
|
||||||
/>
|
>
|
||||||
|
<ng-template #stockTpl let-item>
|
||||||
|
<app-shared-simple-stock-text-alert [stock]="item.stock" [minimumStockAlertLevel]="item.minimumStockAlertLevel" />
|
||||||
|
</ng-template>
|
||||||
|
</app-page-data-list>
|
||||||
|
|
||||||
<product-form [(visible)]="visibleForm" (onSubmit)="refresh()" />
|
<product-form [(visible)]="visibleForm" (onSubmit)="refresh()" />
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ 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 { SimpleStockTextAlertComponent } from '@/shared/components/simpleStockTextAlert/simple-stock-text-alert.component';
|
||||||
|
import { AfterViewInit, Component, signal, TemplateRef, ViewChild } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { ProductFormComponent } from '../components/form.component';
|
import { ProductFormComponent } from '../components/form.component';
|
||||||
import { productsNamedRoutes } from '../constants';
|
import { productsNamedRoutes } from '../constants';
|
||||||
@@ -13,10 +14,13 @@ import { ProductsStore } from '../store/main';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-products',
|
selector: 'app-products',
|
||||||
templateUrl: './list.component.html',
|
templateUrl: './list.component.html',
|
||||||
imports: [PageDataListComponent, ProductFormComponent],
|
imports: [PageDataListComponent, ProductFormComponent, SimpleStockTextAlertComponent],
|
||||||
standalone: true,
|
|
||||||
})
|
})
|
||||||
export class ProductsComponent {
|
export class ProductsComponent implements AfterViewInit {
|
||||||
|
@ViewChild('stockTpl', { static: true }) stockTpl!: TemplateRef<any>;
|
||||||
|
|
||||||
|
columns = signal<IColumn[]>([]);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private productService: ProductsService,
|
private productService: ProductsService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@@ -25,7 +29,8 @@ export class ProductsComponent {
|
|||||||
store.initial();
|
store.initial();
|
||||||
}
|
}
|
||||||
|
|
||||||
columns = [
|
ngAfterViewInit() {
|
||||||
|
this.columns.set([
|
||||||
{ field: 'sku', header: 'شناسه' },
|
{ field: 'sku', header: 'شناسه' },
|
||||||
{ field: 'name', header: 'نام' },
|
{ field: 'name', header: 'نام' },
|
||||||
{
|
{
|
||||||
@@ -47,9 +52,15 @@ export class ProductsComponent {
|
|||||||
header: 'قیمت پایه فروش',
|
header: 'قیمت پایه فروش',
|
||||||
type: 'price',
|
type: 'price',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'stockss',
|
||||||
|
header: 'موجودی کلی',
|
||||||
|
customDataModel: this.stockTpl,
|
||||||
|
},
|
||||||
{ field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' },
|
{ field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' },
|
||||||
{ field: 'updatedAt', header: 'تاریخ بهروزرسانی', type: 'date' },
|
{ field: 'updatedAt', header: 'تاریخ بهروزرسانی', type: 'date' },
|
||||||
] as IColumn[];
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
get loading() {
|
get loading() {
|
||||||
return this.store.loading;
|
return this.store.loading;
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="flex items-center">
|
@if (loading()) {
|
||||||
<div class="grow">
|
<p-progressSpinner />
|
||||||
<div class="flex items-center gap-2">
|
} @else if (product()) {
|
||||||
<p-button icon="pi pi-arrow-right" aria-label="بازگشت" outlined [routerLink]="['/products']" />
|
<app-inner-pages-header [pageTitle]="product()?.name!" [backRoute]="['/products']">
|
||||||
<h3 class="m-0">{{ product()?.name }}</h3>
|
<ng-template #actions>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button pButton type="button" label="افزایش موجودی" icon="pi pi-plus" (click)="openChargeForm()"></button>
|
<button pButton type="button" label="افزایش موجودی" icon="pi pi-plus" (click)="openChargeForm()"></button>
|
||||||
<button
|
<button
|
||||||
pButton
|
pButton
|
||||||
@@ -16,8 +13,8 @@
|
|||||||
[routerLink]="['/products', productId, 'update']"
|
[routerLink]="['/products', productId, 'update']"
|
||||||
></button>
|
></button>
|
||||||
<button pButton icon="pi pi-trash" severity="danger" (click)="openDeleteConfirm()"></button>
|
<button pButton icon="pi pi-trash" severity="danger" (click)="openDeleteConfirm()"></button>
|
||||||
</div>
|
</ng-template>
|
||||||
</div>
|
</app-inner-pages-header>
|
||||||
<div class="inline-flex items-center gap-5">
|
<div class="inline-flex items-center gap-5">
|
||||||
<div class="">
|
<div class="">
|
||||||
<app-key-value label="SKU" [value]="product()?.sku || '-'" />
|
<app-key-value label="SKU" [value]="product()?.sku || '-'" />
|
||||||
@@ -29,6 +26,12 @@
|
|||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<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">
|
||||||
|
<details-info-card icon="pi pi-box" label="موجودی">
|
||||||
|
<app-shared-simple-stock-text-alert
|
||||||
|
[stock]="product()?.stock || 0"
|
||||||
|
[minimumStockAlertLevel]="product()?.minimumStockAlertLevel || 0"
|
||||||
|
/>
|
||||||
|
</details-info-card>
|
||||||
@for (item of topBarCardDetails; track $index) {
|
@for (item of topBarCardDetails; track $index) {
|
||||||
<details-info-card [icon]="item.icon" [label]="item.label" [value]="item.value.toString()" />
|
<details-info-card [icon]="item.icon" [label]="item.label" [value]="item.value.toString()" />
|
||||||
}
|
}
|
||||||
@@ -73,4 +76,5 @@
|
|||||||
</p-galleria>
|
</p-galleria>
|
||||||
</div>
|
</div>
|
||||||
<purchase-form [(visible)]="isOpenChargeForm" [product]="product()!" />
|
<purchase-form [(visible)]="isOpenChargeForm" [product]="product()!" />
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ 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 { DetailsInfoCardComponent } from '@/shared/components/detailsInfoCard/details-info-card.component';
|
||||||
|
import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component';
|
||||||
|
import { SimpleStockTextAlertComponent } from '@/shared/components/simpleStockTextAlert/simple-stock-text-alert.component';
|
||||||
import priceMaskUtils from '@/utils/price-mask.utils';
|
import priceMaskUtils from '@/utils/price-mask.utils';
|
||||||
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 { 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 { ProgressSpinner } from 'primeng/progressspinner';
|
||||||
import { IProductResponse } from '../models';
|
import { IProductResponse } from '../models';
|
||||||
import { ProductsService } from '../services/main.service';
|
import { ProductsService } from '../services/main.service';
|
||||||
|
|
||||||
@@ -20,9 +23,11 @@ import { ProductsService } from '../services/main.service';
|
|||||||
GalleriaModule,
|
GalleriaModule,
|
||||||
KeyValueComponent,
|
KeyValueComponent,
|
||||||
Card,
|
Card,
|
||||||
Button,
|
|
||||||
PurchaseFormComponent,
|
PurchaseFormComponent,
|
||||||
DetailsInfoCardComponent,
|
DetailsInfoCardComponent,
|
||||||
|
ProgressSpinner,
|
||||||
|
SimpleStockTextAlertComponent,
|
||||||
|
InnerPagesHeaderComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ProductComponent {
|
export class ProductComponent {
|
||||||
@@ -79,11 +84,6 @@ export class ProductComponent {
|
|||||||
|
|
||||||
get topBarCardDetails() {
|
get topBarCardDetails() {
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
icon: 'pi pi-box',
|
|
||||||
label: 'موجودی',
|
|
||||||
value: this.product()?.stock || 0,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: 'pi pi-dollar',
|
icon: 'pi pi-dollar',
|
||||||
label: 'قیمت پایه',
|
label: 'قیمت پایه',
|
||||||
@@ -102,7 +102,7 @@ export class ProductComponent {
|
|||||||
{
|
{
|
||||||
icon: 'pi pi-clock',
|
icon: 'pi pi-clock',
|
||||||
label: 'تعداد فروش',
|
label: 'تعداد فروش',
|
||||||
value: '-',
|
value: this.product()?.salesCount || 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -178,4 +178,8 @@
|
|||||||
<button pButton type="button" label="لغو" outlined [disabled]="form.disabled" (click)="onCancel()"></button>
|
<button pButton type="button" label="لغو" outlined [disabled]="form.disabled" (click)="onCancel()"></button>
|
||||||
<button pButton type="button" label="ثبت رسید" [loading]="form.disabled" (click)="onSubmit()"></button>
|
<button pButton type="button" label="ثبت رسید" [loading]="form.disabled" (click)="onSubmit()"></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (purchaseReceiptResponse()) {
|
||||||
|
<app-purchase-receipt-payment-wrapper [purchaseReceipt]="purchaseReceiptResponse()!" />
|
||||||
|
}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+5
-1
@@ -7,6 +7,7 @@ import { SuppliersSelectComponent } from '@/modules/suppliers/components/select/
|
|||||||
import { ISupplierResponse } from '@/modules/suppliers/models';
|
import { ISupplierResponse } from '@/modules/suppliers/models';
|
||||||
import { InlineEditComponent, InputComponent } from '@/shared/components';
|
import { InlineEditComponent, InputComponent } from '@/shared/components';
|
||||||
import { IColumn } from '@/shared/components/pageDataList/page-data-list.component';
|
import { IColumn } from '@/shared/components/pageDataList/page-data-list.component';
|
||||||
|
import { PurchaseReceiptPaymentWrapperComponent } from '@/shared/components/purchaseReceiptPayment/wrapper.component';
|
||||||
import { PriceAlphabetDirective, PriceMaskDirective } from '@/shared/directives';
|
import { PriceAlphabetDirective, PriceMaskDirective } from '@/shared/directives';
|
||||||
import { UikitFlatpickrJalaliComponent } from '@/uikit';
|
import { UikitFlatpickrJalaliComponent } from '@/uikit';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
@@ -16,7 +17,7 @@ import dayjs from 'dayjs';
|
|||||||
import { ButtonDirective } from 'primeng/button';
|
import { ButtonDirective } from 'primeng/button';
|
||||||
import { Card } from 'primeng/card';
|
import { Card } from 'primeng/card';
|
||||||
import { TableModule } from 'primeng/table';
|
import { TableModule } from 'primeng/table';
|
||||||
import { IPurchaseRequest } from '../../models';
|
import { IPurchaseReceiptResponse, IPurchaseRequest } from '../../models';
|
||||||
import { PurchasesService } from '../../services/main.service';
|
import { PurchasesService } from '../../services/main.service';
|
||||||
import { PurchaseReceiptProductRowComponent } from './purchase-receipt-product-row.component';
|
import { PurchaseReceiptProductRowComponent } from './purchase-receipt-product-row.component';
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ import { PurchaseReceiptProductRowComponent } from './purchase-receipt-product-r
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
UikitFlatpickrJalaliComponent,
|
UikitFlatpickrJalaliComponent,
|
||||||
|
PurchaseReceiptPaymentWrapperComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PurchaseReceiptTemplateComponent {
|
export class PurchaseReceiptTemplateComponent {
|
||||||
@@ -142,6 +144,7 @@ export class PurchaseReceiptTemplateComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
formIsSubmitted = signal(false);
|
formIsSubmitted = signal(false);
|
||||||
|
purchaseReceiptResponse = signal<Maybe<IPurchaseReceiptResponse>>(null);
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.formIsSubmitted.set(false);
|
this.formIsSubmitted.set(false);
|
||||||
@@ -203,6 +206,7 @@ export class PurchaseReceiptTemplateComponent {
|
|||||||
// this.form.reset();
|
// this.form.reset();
|
||||||
this.form.controls.code.setValue(this.form.controls.code.value + '1');
|
this.form.controls.code.setValue(this.form.controls.code.value + '1');
|
||||||
this.formIsSubmitted.set(false);
|
this.formIsSubmitted.set(false);
|
||||||
|
this.purchaseReceiptResponse.set(res);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.form.enable();
|
this.form.enable();
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { Maybe } from '@/core';
|
import { Maybe } from '@/core';
|
||||||
|
|
||||||
export interface IPurchaseRawResponse {
|
export interface IPurchaseReceiptRawResponse {
|
||||||
id: number;
|
id: number;
|
||||||
totalAmount: number;
|
totalAmount: number;
|
||||||
inventoryId: number;
|
inventoryId: number;
|
||||||
@@ -12,7 +12,7 @@ export interface IPurchaseRawResponse {
|
|||||||
deletedAt?: string;
|
deletedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPurchaseResponse extends IPurchaseRawResponse {}
|
export interface IPurchaseReceiptResponse extends IPurchaseReceiptRawResponse {}
|
||||||
|
|
||||||
export interface IPurchaseItemRawResponse {
|
export interface IPurchaseItemRawResponse {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http';
|
|||||||
import { inject, Injectable } from '@angular/core';
|
import { inject, Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { PURCHASES_API_ROUTES } from '../constants';
|
import { PURCHASES_API_ROUTES } from '../constants';
|
||||||
import { IPurchaseRawResponse, IPurchaseRequest, IPurchaseResponse } from '../models';
|
import { IPurchaseReceiptRawResponse, IPurchaseReceiptResponse, IPurchaseRequest } from '../models';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -10,15 +10,15 @@ import { IPurchaseRawResponse, IPurchaseRequest, IPurchaseResponse } from '../mo
|
|||||||
export class PurchasesService {
|
export class PurchasesService {
|
||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
|
|
||||||
getAll(): Observable<{ data: IPurchaseRawResponse[] }> {
|
getAll(): Observable<{ data: IPurchaseReceiptRawResponse[] }> {
|
||||||
return this.http.get<{ data: IPurchaseResponse[] }>(PURCHASES_API_ROUTES.getAll);
|
return this.http.get<{ data: IPurchaseReceiptResponse[] }>(PURCHASES_API_ROUTES.getAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSingle(id: number): Observable<{ data: IPurchaseResponse }> {
|
getSingle(id: number): Observable<{ data: IPurchaseReceiptResponse }> {
|
||||||
return this.http.get<{ data: IPurchaseResponse }>(PURCHASES_API_ROUTES.getSingle(id));
|
return this.http.get<{ data: IPurchaseReceiptResponse }>(PURCHASES_API_ROUTES.getSingle(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
create(data: IPurchaseRequest): Observable<{ data: IPurchaseRawResponse }> {
|
create(data: IPurchaseRequest): Observable<IPurchaseReceiptResponse> {
|
||||||
return this.http.post<{ data: IPurchaseResponse }>(PURCHASES_API_ROUTES.create, data);
|
return this.http.post<IPurchaseReceiptRawResponse>(PURCHASES_API_ROUTES.create, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
|
||||||
|
<app-inventory-bank-account-select [control]="form.controls.bankAccountId" [inventoryId]="inventoryId" />
|
||||||
|
<app-catalog-payment-method-type-select-field [control]="form.controls.paymentMethod" />
|
||||||
|
<app-input
|
||||||
|
label="مبلغ پرداختی"
|
||||||
|
[control]="form.controls.amount"
|
||||||
|
name="amount"
|
||||||
|
prefix="تومان"
|
||||||
|
type="price"
|
||||||
|
[hint]="amountFieldHint()"
|
||||||
|
/>
|
||||||
|
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { InventoryBankAccountSelectComponent } from '@/modules/inventories/components/bankAccounts/select.component';
|
||||||
|
import { PaymentMethodTypeSelectComponent } from '@/shared/catalog/paymentMethodTypes/components';
|
||||||
|
import { PaymentType } from '@/shared/catalog/paymentTypes';
|
||||||
|
import { InputComponent } from '@/shared/components';
|
||||||
|
import { AbstractForm } from '@/shared/components/abstract-form';
|
||||||
|
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
||||||
|
import { formatNumber } from '@/utils/price-mask.utils';
|
||||||
|
import { Component, computed, inject, Input } from '@angular/core';
|
||||||
|
import { ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { Dialog } from 'primeng/dialog';
|
||||||
|
import { IPayRequestPayload, IPayResponse } from '../../models/invoices.io';
|
||||||
|
import { SupplierInvoicesService } from '../../services';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-supplier-invoice-pay-form-template',
|
||||||
|
templateUrl: './pay-form-template.component.html',
|
||||||
|
imports: [
|
||||||
|
Dialog,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
InventoryBankAccountSelectComponent,
|
||||||
|
FormFooterActionsComponent,
|
||||||
|
InputComponent,
|
||||||
|
PaymentMethodTypeSelectComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class SupplierInvoicePayFormTemplateComponent extends AbstractForm<
|
||||||
|
IPayResponse,
|
||||||
|
IPayRequestPayload
|
||||||
|
> {
|
||||||
|
@Input() debtAmount!: number;
|
||||||
|
@Input() supplierId!: string;
|
||||||
|
@Input() invoiceId!: string;
|
||||||
|
@Input() inventoryId!: string;
|
||||||
|
|
||||||
|
private readonly service = inject(SupplierInvoicesService);
|
||||||
|
|
||||||
|
override defaultValues = {
|
||||||
|
type: PaymentType.PAYMENT,
|
||||||
|
payedAt: dayjs().format('YYYY-MM-DD'),
|
||||||
|
};
|
||||||
|
|
||||||
|
form = this.fb.group({
|
||||||
|
amount: [0, [Validators.required, Validators.max(this.debtAmount), Validators.min(1)]],
|
||||||
|
bankAccountId: [null, [Validators.required]],
|
||||||
|
paymentMethod: [null, [Validators.required]],
|
||||||
|
type: [PaymentType.PAYMENT, [Validators.required]],
|
||||||
|
payedAt: [dayjs().format('YYYY-MM-DD'), [Validators.required]],
|
||||||
|
description: [''],
|
||||||
|
});
|
||||||
|
|
||||||
|
amountFieldHint = computed(() => `بدهی شما مبلغ ${formatNumber(this.debtAmount)} ریال میباشد.`);
|
||||||
|
|
||||||
|
submitForm(payload: IPayRequestPayload) {
|
||||||
|
return this.service.pay(this.supplierId, this.invoiceId, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,18 +6,11 @@
|
|||||||
appendTo="body"
|
appendTo="body"
|
||||||
[closable]="true"
|
[closable]="true"
|
||||||
>
|
>
|
||||||
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
|
<app-supplier-invoice-pay-form-template
|
||||||
{{ this.debtAmount }}
|
[initialValues]="initialValues"
|
||||||
<app-inventory-bank-account-select [control]="form.controls.bankAccountId" [inventoryId]="inventoryId" />
|
[inventoryId]="inventoryId"
|
||||||
<app-catalog-payment-method-type-select-field [control]="form.controls.paymentMethod" />
|
[supplierId]="supplierId"
|
||||||
<app-input
|
[invoiceId]="invoiceId"
|
||||||
label="مبلغ پرداختی"
|
[debtAmount]="debtAmount"
|
||||||
[control]="form.controls.amount"
|
|
||||||
name="amount"
|
|
||||||
prefix="تومان"
|
|
||||||
type="price"
|
|
||||||
[hint]="amountFieldHint()"
|
|
||||||
/>
|
/>
|
||||||
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
|
|
||||||
</form>
|
|
||||||
</p-dialog>
|
</p-dialog>
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { InventoryBankAccountSelectComponent } from '@/modules/inventories/components/bankAccounts/select.component';
|
|
||||||
import { PaymentMethodTypeSelectComponent } from '@/shared/catalog/paymentMethodTypes/components';
|
|
||||||
import { PaymentType } from '@/shared/catalog/paymentTypes';
|
import { PaymentType } from '@/shared/catalog/paymentTypes';
|
||||||
import { InputComponent } from '@/shared/components';
|
|
||||||
import { AbstractFormDialog } from '@/shared/components/abstract-form-dialog.component';
|
import { AbstractFormDialog } from '@/shared/components/abstract-form-dialog.component';
|
||||||
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
|
||||||
import { formatNumber } from '@/utils/price-mask.utils';
|
import { formatNumber } from '@/utils/price-mask.utils';
|
||||||
import { Component, computed, inject, Input } from '@angular/core';
|
import { Component, computed, inject, Input } from '@angular/core';
|
||||||
import { ReactiveFormsModule, Validators } from '@angular/forms';
|
import { ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
@@ -11,18 +7,12 @@ import dayjs from 'dayjs';
|
|||||||
import { Dialog } from 'primeng/dialog';
|
import { Dialog } from 'primeng/dialog';
|
||||||
import { IPayRequestPayload, IPayResponse } from '../../models/invoices.io';
|
import { IPayRequestPayload, IPayResponse } from '../../models/invoices.io';
|
||||||
import { SupplierInvoicesService } from '../../services';
|
import { SupplierInvoicesService } from '../../services';
|
||||||
|
import { SupplierInvoicePayFormTemplateComponent } from './pay-form-template.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-supplier-invoice-pay-form',
|
selector: 'app-supplier-invoice-pay-form',
|
||||||
templateUrl: './pay-form.component.html',
|
templateUrl: './pay-form.component.html',
|
||||||
imports: [
|
imports: [Dialog, ReactiveFormsModule, SupplierInvoicePayFormTemplateComponent],
|
||||||
Dialog,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
InventoryBankAccountSelectComponent,
|
|
||||||
FormFooterActionsComponent,
|
|
||||||
InputComponent,
|
|
||||||
PaymentMethodTypeSelectComponent,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class SupplierInvoicePayFormComponent extends AbstractFormDialog<
|
export class SupplierInvoicePayFormComponent extends AbstractFormDialog<
|
||||||
IPayResponse,
|
IPayResponse,
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ export const suppliersNamedRoutes: NamedRoutes<any> = {
|
|||||||
pagePath: () => '/suppliers/:supplierId/invoices/:invoiceId',
|
pagePath: () => '/suppliers/:supplierId/invoices/:invoiceId',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
purchase: {
|
||||||
|
path: 'suppliers/:supplierId/purchase',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('../../views/purchase.component').then((m) => m.InventoryPurchaseComponent),
|
||||||
|
meta: {
|
||||||
|
title: 'خرید',
|
||||||
|
pagePath: () => '/suppliers/:supplierId/purchase',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type TSuppliersRouteNames = (typeof suppliersNamedRoutes)[keyof typeof suppliersNamedRoutes];
|
export type TSuppliersRouteNames = (typeof suppliersNamedRoutes)[keyof typeof suppliersNamedRoutes];
|
||||||
|
|||||||
@@ -20,6 +20,18 @@
|
|||||||
}
|
}
|
||||||
</app-key-value>
|
</app-key-value>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<app-key-value label="مبلغ کل فاکتور">
|
||||||
|
<span [appPriceMask]="invoice()?.totalAmount"></span>
|
||||||
|
</app-key-value>
|
||||||
|
</div>
|
||||||
|
@if (debtAmount() > 0) {
|
||||||
|
<div class="">
|
||||||
|
<app-key-value label="مبلغ بدهی">
|
||||||
|
<span [appPriceMask]="debtAmount()"></span>
|
||||||
|
</app-key-value>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<app-invoice-payments [supplierId]="supplierId" [invoiceId]="invoiceId" />
|
<app-invoice-payments [supplierId]="supplierId" [invoiceId]="invoiceId" />
|
||||||
@if (invoice() && supplier()) {
|
@if (invoice() && supplier()) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { CatalogPurchaseReceiptStatusTagComponent } from '@/shared/catalog';
|
import { CatalogPurchaseReceiptStatusTagComponent } from '@/shared/catalog';
|
||||||
import { KeyValueComponent } from '@/shared/components';
|
import { KeyValueComponent } from '@/shared/components';
|
||||||
import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component';
|
import { InnerPagesHeaderComponent } from '@/shared/components/innerPagesHeader/inner-pages-header.component';
|
||||||
|
import { PurchaseReceiptPaymentWrapperComponent } from '@/shared/components/purchaseReceiptPayment/wrapper.component';
|
||||||
|
import { PriceMaskDirective } from '@/shared/directives';
|
||||||
import { Component, computed, inject, signal } from '@angular/core';
|
import { Component, computed, inject, signal } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { ButtonDirective } from 'primeng/button';
|
import { ButtonDirective } from 'primeng/button';
|
||||||
@@ -18,6 +20,8 @@ import { SupplierInvoiceStore, SupplierStore } from '../store';
|
|||||||
ButtonDirective,
|
ButtonDirective,
|
||||||
SupplierInvoicePayFormComponent,
|
SupplierInvoicePayFormComponent,
|
||||||
InvoicePaymentsComponent,
|
InvoicePaymentsComponent,
|
||||||
|
PriceMaskDirective,
|
||||||
|
PurchaseReceiptPaymentWrapperComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SupplierInvoiceComponent {
|
export class SupplierInvoiceComponent {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
@if (loading()) {
|
||||||
|
} @else {
|
||||||
|
<purchase-receipt-template [supplier]="supplier()" />
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { PurchaseReceiptTemplateComponent } from '@/modules/purchases/components';
|
||||||
|
import { Component, computed, inject } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { SupplierStore } from '../store';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-inventory-purchase',
|
||||||
|
templateUrl: './purchase.component.html',
|
||||||
|
imports: [PurchaseReceiptTemplateComponent],
|
||||||
|
})
|
||||||
|
export class InventoryPurchaseComponent {
|
||||||
|
private readonly route = inject(ActivatedRoute);
|
||||||
|
private readonly store = inject(SupplierStore);
|
||||||
|
|
||||||
|
supplierId = this.route.snapshot.paramMap.get('supplierId')!;
|
||||||
|
|
||||||
|
supplier = computed(() => this.store.entities()[this.supplierId]);
|
||||||
|
loading = this.store.loading;
|
||||||
|
constructor() {
|
||||||
|
this.store.getSingle(this.supplierId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button pButton type="button" label="خرید کالا" icon="pi pi-plus"></button>
|
<button
|
||||||
|
pButton
|
||||||
|
type="button"
|
||||||
|
label="خرید کالا"
|
||||||
|
icon="pi pi-plus"
|
||||||
|
[routerLink]="['/suppliers', supplierId, 'purchase']"
|
||||||
|
></button>
|
||||||
<button pButton type="button" label="ویرایش" icon="pi pi-pencil"></button>
|
<button pButton type="button" label="ویرایش" icon="pi pi-pencil"></button>
|
||||||
<button pButton icon="pi pi-trash" severity="danger"></button>
|
<button pButton icon="pi pi-trash" severity="danger"></button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import { ToastService } from '@/core/services/toast.service';
|
import { ToastService } from '@/core/services/toast.service';
|
||||||
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 } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { Subscribable } from 'rxjs';
|
import { AbstractForm } from './abstract-form';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'abstract-form-dialog',
|
selector: 'abstract-form-dialog',
|
||||||
template: '',
|
template: '',
|
||||||
imports: [ReactiveFormsModule],
|
imports: [ReactiveFormsModule],
|
||||||
})
|
})
|
||||||
export abstract class AbstractFormDialog<Response, Request> {
|
export abstract class AbstractFormDialog<Response, Request> extends AbstractForm<
|
||||||
@Input() initialValues?: Response;
|
Response,
|
||||||
|
Request
|
||||||
|
> {
|
||||||
@Input()
|
@Input()
|
||||||
set visible(v: boolean) {
|
set visible(v: boolean) {
|
||||||
this.visibleSignal.set(!!v);
|
this.visibleSignal.set(!!v);
|
||||||
|
this.visibleChange.emit(!!v);
|
||||||
}
|
}
|
||||||
get visible() {
|
get visible() {
|
||||||
return this.visibleSignal();
|
return this.visibleSignal();
|
||||||
@@ -21,58 +23,19 @@ export abstract class AbstractFormDialog<Response, Request> {
|
|||||||
private visibleSignal = signal(false);
|
private visibleSignal = signal(false);
|
||||||
|
|
||||||
@Output() visibleChange = new EventEmitter<boolean>();
|
@Output() visibleChange = new EventEmitter<boolean>();
|
||||||
@Output() onSubmit = new EventEmitter<Response>();
|
|
||||||
|
|
||||||
protected readonly fb = inject(FormBuilder);
|
constructor() {
|
||||||
constructor(private toastService: ToastService) {
|
super(inject(ToastService));
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const v = this.visibleSignal();
|
const v = this.visibleSignal();
|
||||||
if (!v) this.form.reset(this.defaultValues);
|
if (!v) this.form.reset(this.defaultValues);
|
||||||
});
|
});
|
||||||
}
|
this.submit.bind(() => {
|
||||||
|
|
||||||
abstract form: ReturnType<FormBuilder['group']>;
|
|
||||||
defaultValues: Partial<Request> = {};
|
|
||||||
|
|
||||||
abstract submitForm(payload: Request): Subscribable<Response> | void;
|
|
||||||
|
|
||||||
submitLoading = signal(false);
|
|
||||||
|
|
||||||
submit() {
|
|
||||||
console.log('first');
|
|
||||||
|
|
||||||
this.form.markAllAsTouched();
|
|
||||||
if (this.form.valid) {
|
|
||||||
console.log('isValid');
|
|
||||||
console.log(this.submitForm);
|
|
||||||
|
|
||||||
this.form.disable();
|
|
||||||
this.submitLoading.set(true);
|
|
||||||
this.submitForm(this.form.value as Request)?.subscribe({
|
|
||||||
next: (res) => {
|
|
||||||
this.toastService.success({
|
|
||||||
text: `با موفقیت ایجاد شد`,
|
|
||||||
});
|
|
||||||
this.close();
|
this.close();
|
||||||
this.submitLoading.set(false);
|
|
||||||
this.form.enable();
|
|
||||||
this.form.reset();
|
|
||||||
this.onSubmit.emit(res);
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
this.submitLoading.set(false);
|
|
||||||
this.form.enable();
|
|
||||||
},
|
|
||||||
complete: () => {
|
|
||||||
this.submitLoading.set(false);
|
|
||||||
this.form.enable();
|
|
||||||
console.log('end');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
override close() {
|
||||||
this.visibleChange.emit(false);
|
this.visibleSignal.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { ToastService } from '@/core/services/toast.service';
|
||||||
|
import { Component, EventEmitter, inject, Input, Output, signal } from '@angular/core';
|
||||||
|
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { Subscribable } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'abstract-form-dialog',
|
||||||
|
template: '',
|
||||||
|
imports: [ReactiveFormsModule],
|
||||||
|
})
|
||||||
|
export abstract class AbstractForm<Response, Request> {
|
||||||
|
@Input() initialValues?: Response;
|
||||||
|
|
||||||
|
@Output() onSubmit = new EventEmitter<Response>();
|
||||||
|
@Output() onClose = new EventEmitter<void>();
|
||||||
|
|
||||||
|
protected readonly fb = inject(FormBuilder);
|
||||||
|
constructor(private toastService: ToastService) {}
|
||||||
|
|
||||||
|
abstract form: ReturnType<FormBuilder['group']>;
|
||||||
|
defaultValues: Partial<Request> = {};
|
||||||
|
|
||||||
|
abstract submitForm(payload: Request): Subscribable<Response> | void;
|
||||||
|
|
||||||
|
submitLoading = signal(false);
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
this.form.markAllAsTouched();
|
||||||
|
if (this.form.valid) {
|
||||||
|
this.form.disable();
|
||||||
|
this.submitLoading.set(true);
|
||||||
|
this.submitForm(this.form.value as Request)?.subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.toastService.success({
|
||||||
|
text: `با موفقیت ایجاد شد`,
|
||||||
|
});
|
||||||
|
this.submitLoading.set(false);
|
||||||
|
this.form.enable();
|
||||||
|
this.form.reset();
|
||||||
|
this.onSubmit.emit(res);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.submitLoading.set(false);
|
||||||
|
this.form.enable();
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
this.submitLoading.set(false);
|
||||||
|
this.form.enable();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.onClose.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<p-confirmdialog #cd>
|
||||||
|
<ng-template #headless let-message let-onAccept="onAccept" let-onReject="onReject">
|
||||||
|
@if (message) {
|
||||||
|
<div class="flex flex-col items-center p-8 bg-surface-0 dark:bg-surface-900 rounded">
|
||||||
|
<div
|
||||||
|
class="rounded-full bg-primary text-primary-contrast inline-flex justify-center items-center h-24 w-24 -mt-20"
|
||||||
|
>
|
||||||
|
<i class="pi pi-question text-5xl!"></i>
|
||||||
|
</div>
|
||||||
|
<span class="font-bold text-2xl block mb-2 mt-6">{{ message.header }}</span>
|
||||||
|
<p class="mb-0">{{ message.message }}</p>
|
||||||
|
<div class="flex items-center gap-2 mt-6">
|
||||||
|
<p-button label="{{ acceptLabel }}" (onClick)="onAccept()" styleClass="w-32"></p-button>
|
||||||
|
<p-button label="{{ rejectLabel }}" [outlined]="true" (onClick)="onReject()" styleClass="w-32"></p-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
|
</p-confirmdialog>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
|
||||||
|
import { ConfirmationService } from 'primeng/api';
|
||||||
|
import { Button } from 'primeng/button';
|
||||||
|
import { ConfirmDialog } from 'primeng/confirmdialog';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-shared-confirmation-dialog',
|
||||||
|
templateUrl: './confirmation-dialog.component.html',
|
||||||
|
providers: [ConfirmationService],
|
||||||
|
imports: [ConfirmDialog, Button],
|
||||||
|
})
|
||||||
|
export class ConfirmationDialogComponent implements OnDestroy {
|
||||||
|
@Input() message!: string;
|
||||||
|
@Input() header: string = 'تأیید عملیات';
|
||||||
|
@Input() acceptLabel: string = 'بله';
|
||||||
|
@Input() rejectLabel: string = 'خیر';
|
||||||
|
@Output() onAccept = new EventEmitter<void>();
|
||||||
|
@Output() onReject = new EventEmitter<void>();
|
||||||
|
|
||||||
|
constructor(private confirmationService: ConfirmationService) {}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.confirmationService.confirm({
|
||||||
|
header: this.header,
|
||||||
|
message: this.message,
|
||||||
|
acceptLabel: this.acceptLabel,
|
||||||
|
rejectLabel: this.rejectLabel,
|
||||||
|
accept: () => {
|
||||||
|
this.accept();
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
this.reject();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
accept() {
|
||||||
|
this.onAccept.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
reject() {
|
||||||
|
this.onReject.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
// Cleanup if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
ApplicationRef,
|
||||||
|
ComponentFactoryResolver,
|
||||||
|
ComponentRef,
|
||||||
|
Injectable,
|
||||||
|
Injector,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { ConfirmationDialogComponent } from './confirmation-dialog.component';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class ConfirmationDialogService {
|
||||||
|
private componentRef: ComponentRef<ConfirmationDialogComponent> | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private componentFactoryResolver: ComponentFactoryResolver,
|
||||||
|
private appRef: ApplicationRef,
|
||||||
|
private injector: Injector,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
confirm(options: {
|
||||||
|
message: string;
|
||||||
|
header?: string;
|
||||||
|
acceptLabel?: string;
|
||||||
|
rejectLabel?: string;
|
||||||
|
accept?: () => void;
|
||||||
|
reject?: () => void;
|
||||||
|
}) {
|
||||||
|
// Create the component dynamically
|
||||||
|
const factory = this.componentFactoryResolver.resolveComponentFactory(
|
||||||
|
ConfirmationDialogComponent,
|
||||||
|
);
|
||||||
|
this.componentRef = factory.create(this.injector);
|
||||||
|
|
||||||
|
// Set inputs
|
||||||
|
this.componentRef.instance.message = options.message;
|
||||||
|
if (options.header) this.componentRef.instance.header = options.header;
|
||||||
|
if (options.acceptLabel) this.componentRef.instance.acceptLabel = options.acceptLabel;
|
||||||
|
if (options.rejectLabel) this.componentRef.instance.rejectLabel = options.rejectLabel;
|
||||||
|
|
||||||
|
// Subscribe to outputs and close after
|
||||||
|
if (options.accept) {
|
||||||
|
this.componentRef.instance.onAccept.subscribe(() => {
|
||||||
|
options.accept!();
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.componentRef.instance.onAccept.subscribe(() => this.close());
|
||||||
|
}
|
||||||
|
if (options.reject) {
|
||||||
|
this.componentRef.instance.onReject.subscribe(() => {
|
||||||
|
options.reject!();
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.componentRef.instance.onReject.subscribe(() => this.close());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach to the app
|
||||||
|
this.appRef.attachView(this.componentRef.hostView);
|
||||||
|
|
||||||
|
// Append to body
|
||||||
|
document.body.appendChild(this.componentRef.location.nativeElement);
|
||||||
|
|
||||||
|
// Trigger change detection and show
|
||||||
|
this.componentRef.changeDetectorRef.detectChanges();
|
||||||
|
this.componentRef.instance.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (this.componentRef) {
|
||||||
|
this.appRef.detachView(this.componentRef.hostView);
|
||||||
|
this.componentRef.destroy();
|
||||||
|
this.componentRef = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grow flex flex-col gap-1">
|
<div class="grow flex flex-col gap-1">
|
||||||
<span class="text-sm font-bold">{{ label }}</span>
|
<span class="text-sm font-bold">{{ label }}</span>
|
||||||
|
<ng-content>
|
||||||
<span class="text-xl font-bold text-primary">{{ value }}</span>
|
<span class="text-xl font-bold text-primary">{{ value }}</span>
|
||||||
|
</ng-content>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</p-card>
|
</p-card>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Maybe } from '@/core';
|
||||||
|
import { Component, ElementRef, Input, ViewChild } from '@angular/core';
|
||||||
import { Card } from 'primeng/card';
|
import { Card } from 'primeng/card';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -10,5 +11,7 @@ export class DetailsInfoCardComponent {
|
|||||||
@Input() icon: string = '';
|
@Input() icon: string = '';
|
||||||
@Input() label: string = '';
|
@Input() label: string = '';
|
||||||
@Input() value: string = '';
|
@Input() value: string = '';
|
||||||
|
|
||||||
|
@ViewChild('valueElement') valueElement: Maybe<ElementRef> = null;
|
||||||
constructor() {}
|
constructor() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
@if (showPaymentForm()) {
|
||||||
|
<app-supplier-invoice-pay-form
|
||||||
|
[(visible)]="showPaymentForm"
|
||||||
|
[debtAmount]="purchaseReceipt.totalAmount"
|
||||||
|
[supplierId]="purchaseReceipt.supplierId.toString()"
|
||||||
|
[inventoryId]="purchaseReceipt.inventoryId.toString()"
|
||||||
|
[invoiceId]="purchaseReceipt.id.toString()"
|
||||||
|
/>
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { ToastService } from '@/core/services/toast.service';
|
||||||
|
import { SupplierInvoicePayFormComponent } from '@/modules/suppliers/components/invoices/pay-form.component';
|
||||||
|
import { Component, Input, signal } from '@angular/core';
|
||||||
|
import { IPurchaseReceiptResponse } from '../../../modules/purchases/models';
|
||||||
|
import { ConfirmationDialogService } from '../confirmationDialog/confirmation-dialog.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-purchase-receipt-payment-wrapper',
|
||||||
|
templateUrl: './wrapper.component.html',
|
||||||
|
imports: [SupplierInvoicePayFormComponent],
|
||||||
|
})
|
||||||
|
export class PurchaseReceiptPaymentWrapperComponent {
|
||||||
|
@Input() purchaseReceipt!: IPurchaseReceiptResponse;
|
||||||
|
|
||||||
|
showPaymentForm = signal(false);
|
||||||
|
constructor(
|
||||||
|
private confirmService: ConfirmationDialogService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.showConfirm();
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
showConfirm() {
|
||||||
|
this.confirmService.confirm({
|
||||||
|
message: 'آیا پرداخت فاکتور انجام شده است؟',
|
||||||
|
header: 'وضعیت پرداخت فاکتور',
|
||||||
|
acceptLabel: 'بله',
|
||||||
|
rejectLabel: 'خیر',
|
||||||
|
accept: () => {
|
||||||
|
this.toPaymentForm();
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
this.toastService.info({
|
||||||
|
text: 'میتوانید از طریق منوی پرداخت فاکتور، در آینده اقدام به پرداخت نمایید.',
|
||||||
|
title: 'پرداخت فاکتور انجام نشد',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
submit() {}
|
||||||
|
|
||||||
|
toPaymentForm() {
|
||||||
|
this.showPaymentForm.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<span [class]="'text-xl font-bold ' + (stock! <= minimumStockAlertLevel! || !stock ? 'text-error' : 'text-primary')">
|
||||||
|
{{ stock || 0 }}
|
||||||
|
|
||||||
|
@if (stock! <= minimumStockAlertLevel! || !stock) {
|
||||||
|
<i class="pi pi-exclamation-triangle ml-2" pTooltip="موجودی کمتر از حداقل سطح هشدار است"></i>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { Tooltip } from 'primeng/tooltip';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-shared-simple-stock-text-alert',
|
||||||
|
templateUrl: './simple-stock-text-alert.component.html',
|
||||||
|
imports: [Tooltip],
|
||||||
|
})
|
||||||
|
export class SimpleStockTextAlertComponent {
|
||||||
|
@Input() stock: number = 0;
|
||||||
|
@Input() minimumStockAlertLevel: number = 0;
|
||||||
|
constructor() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<div class="card mb-0">
|
||||||
|
<div class="flex justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<span class="block text-muted-color font-medium mb-4">Orders</span>
|
||||||
|
<div class="text-surface-900 dark:text-surface-0 font-medium text-xl">152</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center bg-blue-100 dark:bg-blue-400/10 rounded-border"
|
||||||
|
style="width: 2.5rem; height: 2.5rem"
|
||||||
|
>
|
||||||
|
<i class="pi pi-shopping-cart text-blue-500 text-xl!"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-primary font-medium">24 new </span>
|
||||||
|
<span class="text-muted-color">since last visit</span>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: 'app-shared-stat-widget',
|
||||||
|
imports: [CommonModule],
|
||||||
|
templateUrl: './stat-card.component.html',
|
||||||
|
})
|
||||||
|
export class StatWidget {}
|
||||||
Reference in New Issue
Block a user