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 { RouterModule } from '@angular/router';
|
||||
import { ConfirmDialog } from 'primeng/confirmdialog';
|
||||
import { ToastModule } from 'primeng/toast';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [RouterModule, ToastModule],
|
||||
imports: [RouterModule, ToastModule, ConfirmDialog],
|
||||
template: `
|
||||
<div>
|
||||
<p-toast position="bottom-right" />
|
||||
<p-confirmDialog />
|
||||
<router-outlet />
|
||||
</div>
|
||||
`,
|
||||
|
||||
+2
-1
@@ -20,7 +20,7 @@ import {
|
||||
withInMemoryScrolling,
|
||||
} from '@angular/router';
|
||||
// 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 { appRoutes } from './app.routes';
|
||||
import MyPreset from './presets';
|
||||
@@ -66,6 +66,7 @@ export const appConfig: ApplicationConfig = {
|
||||
},
|
||||
|
||||
MessageService,
|
||||
ConfirmationService,
|
||||
|
||||
provideHttpClient(
|
||||
withFetch(),
|
||||
|
||||
@@ -8,6 +8,7 @@ export const INVENTORIES_API_ROUTES = {
|
||||
inventoryMovements: (inventoryId: string) => `${baseUrl}/${inventoryId}/movements`,
|
||||
transfer: () => `${baseUrl}/transfers`,
|
||||
stock: (inventoryId: string) => `${baseUrl}/${inventoryId}/stock`,
|
||||
cardex: (inventoryId: string) => `${baseUrl}/${inventoryId}/cardex`,
|
||||
productCardex: (inventoryId: string, productId: string) =>
|
||||
`${baseUrl}/${inventoryId}/products/${productId}/cardex`,
|
||||
bankAccounts: (inventoryId: number) => `${baseUrl}/${inventoryId}/bank-accounts`,
|
||||
|
||||
@@ -37,12 +37,21 @@ export const inventoriesNamedRoutes: NamedRoutes<any> = {
|
||||
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: {
|
||||
path: 'inventories/:inventoryId/products/:productId/cardex',
|
||||
loadComponent: () =>
|
||||
import('../../views/product-cardex.component').then((m) => m.InventoryProductCardexComponent),
|
||||
meta: {
|
||||
title: 'کارتکس محصول',
|
||||
title: 'کاردکس انبار محصول',
|
||||
pagePath: () => '/inventories/:inventoryId/products/:productId/cardex',
|
||||
},
|
||||
},
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
import { ICardexResponse } from '@/modules/cardex/models';
|
||||
import {
|
||||
IInventoryInfo,
|
||||
IInventoryMovement,
|
||||
@@ -61,6 +62,6 @@ export interface IInventoryStockQuery {
|
||||
isAvailable?: boolean;
|
||||
}
|
||||
|
||||
export interface IInventoryProductCardexRawResponse extends IInventoryStockMovementRawResponse {}
|
||||
export interface IInventoryProductCardexRawResponse extends ICardexResponse {}
|
||||
|
||||
export interface IInventoryProductCardexResponse extends IInventoryProductCardexRawResponse {}
|
||||
|
||||
@@ -72,4 +72,9 @@ export class InventoriesService {
|
||||
`${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"
|
||||
emptyPlaceholderTitle="نقطه فروش برای این انبار تعریف نشده است"
|
||||
emptyPlaceholderDescription="برای تعریف نقطهی فروش، روی دکمهٔ بالا کلیک کنید."
|
||||
[showDetails]="true"
|
||||
[items]="items()"
|
||||
[loading]="loading()"
|
||||
(onAdd)="openAddForm()"
|
||||
(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) {
|
||||
<app-inventory-pos-account-form [(visible)]="visibleForm" [inventoryId]="inventoryId" />
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ import {
|
||||
IColumn,
|
||||
PageDataListComponent,
|
||||
} 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 { ButtonDirective } from 'primeng/button';
|
||||
import { Button, ButtonDirective } from 'primeng/button';
|
||||
import { Card } from 'primeng/card';
|
||||
import { InventoryPosAccountFormComponent } from '../../components/posAccounts/form.component';
|
||||
import { IInventoryPosAccountResponse } from '../../models';
|
||||
@@ -21,6 +22,8 @@ import { InventoryStore } from '../../store/inventory.store';
|
||||
ButtonDirective,
|
||||
InventoryPosAccountFormComponent,
|
||||
InnerPagesHeaderComponent,
|
||||
PriceMaskDirective,
|
||||
Button,
|
||||
],
|
||||
})
|
||||
export class InventoryPosAccountListComponent {
|
||||
@@ -34,9 +37,13 @@ export class InventoryPosAccountListComponent {
|
||||
visibleForm = signal<boolean>(false);
|
||||
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>[];
|
||||
|
||||
pageTitle = computed(() => `نقاط فروش متصل به انبار ${this.inventory()?.name || ''}`);
|
||||
|
||||
ngOnInit() {
|
||||
this.inventoryStore.getSingle(this.inventoryId);
|
||||
this.getItems();
|
||||
@@ -55,13 +62,18 @@ export class InventoryPosAccountListComponent {
|
||||
return `${item.bankAccount.name} - بانک ${item.bankAccount.branch.bank.name} - شعبهی ${item.bankAccount.branch.name}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'salesInfo',
|
||||
header: 'جزییات فروش امروز',
|
||||
customDataModel: this.salesInfoTpl,
|
||||
},
|
||||
|
||||
// {
|
||||
// field: 'actions',
|
||||
// header: '',
|
||||
// customDataModel: this.actionsTpl,
|
||||
// width: '100px',
|
||||
// },
|
||||
{
|
||||
field: 'actions',
|
||||
header: '',
|
||||
customDataModel: this.actionsTpl,
|
||||
width: '100px',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -89,4 +101,6 @@ export class InventoryPosAccountListComponent {
|
||||
toPos(item: IInventoryPosAccountResponse) {
|
||||
window.open(`/pos/${item.id}`, '_blank');
|
||||
}
|
||||
|
||||
toPosInfo(item: IInventoryPosAccountResponse) {}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<shared-cardex [loading]="getCardexLoading()" [items]="cardex() || []" />
|
||||
<cardex-templates-inventory [loading]="getCardexLoading()" [items]="cardex() || []" variant="general" />
|
||||
</p-card>
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Maybe } from '@/core';
|
||||
import { CardexComponent } from '@/modules/cardex/components/templates/inventory.component';
|
||||
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 { Component, computed, 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 { IInventoryProductCardexResponse } from '../models';
|
||||
import { InventoriesService } from '../services/main.service';
|
||||
import { InventoryStore } from '../store/inventory.store';
|
||||
|
||||
@Component({
|
||||
selector: 'inventory-product-cardex',
|
||||
@@ -16,20 +17,15 @@ import { InventoriesService } from '../services/main.service';
|
||||
})
|
||||
export class InventoryProductCardexComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
constructor(
|
||||
private service: InventoriesService,
|
||||
private productService: ProductsService,
|
||||
) {
|
||||
this.getInventory();
|
||||
this.getProduct();
|
||||
this.getCardex();
|
||||
}
|
||||
private readonly store = inject(InventoryStore);
|
||||
|
||||
inventoryId = this.route.snapshot.params['inventoryId'];
|
||||
inventoryId = this.route.snapshot.paramMap.get('inventoryId')!;
|
||||
productId = this.route.snapshot.params['productId'];
|
||||
|
||||
inventory = computed(() => this.store.entities()[this.inventoryId]);
|
||||
loading = this.store.loading;
|
||||
|
||||
getInventoryLoading = signal(false);
|
||||
inventory = signal<Maybe<IInventoryDetailResponse>>(null);
|
||||
getProductLoading = signal(false);
|
||||
product = signal<Maybe<IProductResponse>>(null);
|
||||
|
||||
@@ -40,17 +36,13 @@ export class InventoryProductCardexComponent {
|
||||
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);
|
||||
},
|
||||
});
|
||||
constructor(
|
||||
private service: InventoriesService,
|
||||
private productService: ProductsService,
|
||||
) {
|
||||
this.store.getSingle(this.inventoryId);
|
||||
this.getProduct();
|
||||
this.getCardex();
|
||||
}
|
||||
|
||||
getProduct() {
|
||||
|
||||
@@ -46,6 +46,11 @@
|
||||
</p-card>
|
||||
<p-card header="اطلاعات تکمیلی">
|
||||
<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" />
|
||||
</form>
|
||||
</p-card>
|
||||
|
||||
@@ -55,6 +55,10 @@ export class ProductFullFormComponent {
|
||||
barcode: [this.initialData?.barcode || null],
|
||||
description: [this.initialData?.description || null],
|
||||
categoryId: [this.initialData?.category?.id || null],
|
||||
minimumStockAlertLevel: [
|
||||
this.initialData?.minimumStockAlertLevel || 1,
|
||||
[Validators.required, Validators.min(0)],
|
||||
],
|
||||
imageUrl: [null],
|
||||
salePrice: [this.initialData?.salePrice || 0, [Validators.required, Validators.min(0)]],
|
||||
});
|
||||
@@ -66,7 +70,7 @@ export class ProductFullFormComponent {
|
||||
submit() {
|
||||
this.form.markAsTouched();
|
||||
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 { 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 {}
|
||||
|
||||
@@ -10,17 +11,17 @@ export interface IProductRawResponse {
|
||||
description?: string;
|
||||
barcode?: Maybe<string>;
|
||||
sku: string;
|
||||
// productType: string;
|
||||
brand?: IProductBrand;
|
||||
category?: IProductCategory;
|
||||
// supplierId: number;
|
||||
brand?: ISummary;
|
||||
category?: ISummary;
|
||||
salePrice?: string;
|
||||
count?: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
deletedAt?: Maybe<Date>;
|
||||
stock: number;
|
||||
avgCost?: number;
|
||||
stockBalances: IProductStockBalanceSummary[];
|
||||
salesCount: number;
|
||||
minimumStockAlertLevel: number;
|
||||
}
|
||||
|
||||
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
|
||||
[pageTitle]="'مدیریت کالاها'"
|
||||
[addNewCtaLabel]="'افزودن کالا جدید'"
|
||||
[columns]="columns"
|
||||
[columns]="columns()"
|
||||
[showAdd]="true"
|
||||
emptyPlaceholderTitle="کالای یافت نشد"
|
||||
emptyPlaceholderDescription="برای افزودن کالا جدید، روی دکمهٔ بالا کلیک کنید."
|
||||
@@ -14,6 +14,10 @@
|
||||
(onAdd)="openAddForm()"
|
||||
(onDetails)="toDetails($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()" />
|
||||
|
||||
@@ -2,7 +2,8 @@ import {
|
||||
IColumn,
|
||||
PageDataListComponent,
|
||||
} 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 { ProductFormComponent } from '../components/form.component';
|
||||
import { productsNamedRoutes } from '../constants';
|
||||
@@ -13,10 +14,13 @@ import { ProductsStore } from '../store/main';
|
||||
@Component({
|
||||
selector: 'app-products',
|
||||
templateUrl: './list.component.html',
|
||||
imports: [PageDataListComponent, ProductFormComponent],
|
||||
standalone: true,
|
||||
imports: [PageDataListComponent, ProductFormComponent, SimpleStockTextAlertComponent],
|
||||
})
|
||||
export class ProductsComponent {
|
||||
export class ProductsComponent implements AfterViewInit {
|
||||
@ViewChild('stockTpl', { static: true }) stockTpl!: TemplateRef<any>;
|
||||
|
||||
columns = signal<IColumn[]>([]);
|
||||
|
||||
constructor(
|
||||
private productService: ProductsService,
|
||||
private router: Router,
|
||||
@@ -25,7 +29,8 @@ export class ProductsComponent {
|
||||
store.initial();
|
||||
}
|
||||
|
||||
columns = [
|
||||
ngAfterViewInit() {
|
||||
this.columns.set([
|
||||
{ field: 'sku', header: 'شناسه' },
|
||||
{ field: 'name', header: 'نام' },
|
||||
{
|
||||
@@ -47,9 +52,15 @@ export class ProductsComponent {
|
||||
header: 'قیمت پایه فروش',
|
||||
type: 'price',
|
||||
},
|
||||
{
|
||||
field: 'stockss',
|
||||
header: 'موجودی کلی',
|
||||
customDataModel: this.stockTpl,
|
||||
},
|
||||
{ field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' },
|
||||
{ field: 'updatedAt', header: 'تاریخ بهروزرسانی', type: 'date' },
|
||||
] as IColumn[];
|
||||
]);
|
||||
}
|
||||
|
||||
get loading() {
|
||||
return this.store.loading;
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
<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]="['/products']" />
|
||||
<h3 class="m-0">{{ product()?.name }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@if (loading()) {
|
||||
<p-progressSpinner />
|
||||
} @else if (product()) {
|
||||
<app-inner-pages-header [pageTitle]="product()?.name!" [backRoute]="['/products']">
|
||||
<ng-template #actions>
|
||||
<button pButton type="button" label="افزایش موجودی" icon="pi pi-plus" (click)="openChargeForm()"></button>
|
||||
<button
|
||||
pButton
|
||||
@@ -16,8 +13,8 @@
|
||||
[routerLink]="['/products', productId, 'update']"
|
||||
></button>
|
||||
<button pButton icon="pi pi-trash" severity="danger" (click)="openDeleteConfirm()"></button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-inner-pages-header>
|
||||
<div class="inline-flex items-center gap-5">
|
||||
<div class="">
|
||||
<app-key-value label="SKU" [value]="product()?.sku || '-'" />
|
||||
@@ -29,6 +26,12 @@
|
||||
<div class="flex gap-4">
|
||||
<div class="flex grow flex-col 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) {
|
||||
<details-info-card [icon]="item.icon" [label]="item.label" [value]="item.value.toString()" />
|
||||
}
|
||||
@@ -73,4 +76,5 @@
|
||||
</p-galleria>
|
||||
</div>
|
||||
<purchase-form [(visible)]="isOpenChargeForm" [product]="product()!" />
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,15 @@ import { Maybe } from '@/core';
|
||||
import { PurchaseFormComponent } from '@/modules/purchases/components/form.component';
|
||||
import { KeyValueComponent } from '@/shared/components';
|
||||
import { DetailsInfoCardComponent } from '@/shared/components/detailsInfoCard/details-info-card.component';
|
||||
import { 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 { Component, inject, signal } from '@angular/core';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { Button, ButtonDirective } from 'primeng/button';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { Card } from 'primeng/card';
|
||||
import { GalleriaModule } from 'primeng/galleria';
|
||||
import { ProgressSpinner } from 'primeng/progressspinner';
|
||||
import { IProductResponse } from '../models';
|
||||
import { ProductsService } from '../services/main.service';
|
||||
|
||||
@@ -20,9 +23,11 @@ import { ProductsService } from '../services/main.service';
|
||||
GalleriaModule,
|
||||
KeyValueComponent,
|
||||
Card,
|
||||
Button,
|
||||
PurchaseFormComponent,
|
||||
DetailsInfoCardComponent,
|
||||
ProgressSpinner,
|
||||
SimpleStockTextAlertComponent,
|
||||
InnerPagesHeaderComponent,
|
||||
],
|
||||
})
|
||||
export class ProductComponent {
|
||||
@@ -79,11 +84,6 @@ export class ProductComponent {
|
||||
|
||||
get topBarCardDetails() {
|
||||
return [
|
||||
{
|
||||
icon: 'pi pi-box',
|
||||
label: 'موجودی',
|
||||
value: this.product()?.stock || 0,
|
||||
},
|
||||
{
|
||||
icon: 'pi pi-dollar',
|
||||
label: 'قیمت پایه',
|
||||
@@ -102,7 +102,7 @@ export class ProductComponent {
|
||||
{
|
||||
icon: 'pi pi-clock',
|
||||
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="ثبت رسید" [loading]="form.disabled" (click)="onSubmit()"></button>
|
||||
</div>
|
||||
|
||||
@if (purchaseReceiptResponse()) {
|
||||
<app-purchase-receipt-payment-wrapper [purchaseReceipt]="purchaseReceiptResponse()!" />
|
||||
}
|
||||
</form>
|
||||
|
||||
+5
-1
@@ -7,6 +7,7 @@ import { SuppliersSelectComponent } from '@/modules/suppliers/components/select/
|
||||
import { ISupplierResponse } from '@/modules/suppliers/models';
|
||||
import { InlineEditComponent, InputComponent } from '@/shared/components';
|
||||
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 { UikitFlatpickrJalaliComponent } from '@/uikit';
|
||||
import { CommonModule } from '@angular/common';
|
||||
@@ -16,7 +17,7 @@ import dayjs from 'dayjs';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
import { Card } from 'primeng/card';
|
||||
import { TableModule } from 'primeng/table';
|
||||
import { IPurchaseRequest } from '../../models';
|
||||
import { IPurchaseReceiptResponse, IPurchaseRequest } from '../../models';
|
||||
import { PurchasesService } from '../../services/main.service';
|
||||
import { PurchaseReceiptProductRowComponent } from './purchase-receipt-product-row.component';
|
||||
|
||||
@@ -39,6 +40,7 @@ import { PurchaseReceiptProductRowComponent } from './purchase-receipt-product-r
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
UikitFlatpickrJalaliComponent,
|
||||
PurchaseReceiptPaymentWrapperComponent,
|
||||
],
|
||||
})
|
||||
export class PurchaseReceiptTemplateComponent {
|
||||
@@ -142,6 +144,7 @@ export class PurchaseReceiptTemplateComponent {
|
||||
}
|
||||
|
||||
formIsSubmitted = signal(false);
|
||||
purchaseReceiptResponse = signal<Maybe<IPurchaseReceiptResponse>>(null);
|
||||
|
||||
ngOnInit() {
|
||||
this.formIsSubmitted.set(false);
|
||||
@@ -203,6 +206,7 @@ export class PurchaseReceiptTemplateComponent {
|
||||
// this.form.reset();
|
||||
this.form.controls.code.setValue(this.form.controls.code.value + '1');
|
||||
this.formIsSubmitted.set(false);
|
||||
this.purchaseReceiptResponse.set(res);
|
||||
},
|
||||
error: () => {
|
||||
this.form.enable();
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { Maybe } from '@/core';
|
||||
|
||||
export interface IPurchaseRawResponse {
|
||||
export interface IPurchaseReceiptRawResponse {
|
||||
id: number;
|
||||
totalAmount: number;
|
||||
inventoryId: number;
|
||||
@@ -12,7 +12,7 @@ export interface IPurchaseRawResponse {
|
||||
deletedAt?: string;
|
||||
}
|
||||
|
||||
export interface IPurchaseResponse extends IPurchaseRawResponse {}
|
||||
export interface IPurchaseReceiptResponse extends IPurchaseReceiptRawResponse {}
|
||||
|
||||
export interface IPurchaseItemRawResponse {
|
||||
id: number;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { PURCHASES_API_ROUTES } from '../constants';
|
||||
import { IPurchaseRawResponse, IPurchaseRequest, IPurchaseResponse } from '../models';
|
||||
import { IPurchaseReceiptRawResponse, IPurchaseReceiptResponse, IPurchaseRequest } from '../models';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -10,15 +10,15 @@ import { IPurchaseRawResponse, IPurchaseRequest, IPurchaseResponse } from '../mo
|
||||
export class PurchasesService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
getAll(): Observable<{ data: IPurchaseRawResponse[] }> {
|
||||
return this.http.get<{ data: IPurchaseResponse[] }>(PURCHASES_API_ROUTES.getAll);
|
||||
getAll(): Observable<{ data: IPurchaseReceiptRawResponse[] }> {
|
||||
return this.http.get<{ data: IPurchaseReceiptResponse[] }>(PURCHASES_API_ROUTES.getAll);
|
||||
}
|
||||
|
||||
getSingle(id: number): Observable<{ data: IPurchaseResponse }> {
|
||||
return this.http.get<{ data: IPurchaseResponse }>(PURCHASES_API_ROUTES.getSingle(id));
|
||||
getSingle(id: number): Observable<{ data: IPurchaseReceiptResponse }> {
|
||||
return this.http.get<{ data: IPurchaseReceiptResponse }>(PURCHASES_API_ROUTES.getSingle(id));
|
||||
}
|
||||
|
||||
create(data: IPurchaseRequest): Observable<{ data: IPurchaseRawResponse }> {
|
||||
return this.http.post<{ data: IPurchaseResponse }>(PURCHASES_API_ROUTES.create, data);
|
||||
create(data: IPurchaseRequest): Observable<IPurchaseReceiptResponse> {
|
||||
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"
|
||||
[closable]="true"
|
||||
>
|
||||
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
|
||||
{{ this.debtAmount }}
|
||||
<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-supplier-invoice-pay-form-template
|
||||
[initialValues]="initialValues"
|
||||
[inventoryId]="inventoryId"
|
||||
[supplierId]="supplierId"
|
||||
[invoiceId]="invoiceId"
|
||||
[debtAmount]="debtAmount"
|
||||
/>
|
||||
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
|
||||
</form>
|
||||
</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 { InputComponent } from '@/shared/components';
|
||||
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 { Component, computed, inject, Input } from '@angular/core';
|
||||
import { ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
@@ -11,18 +7,12 @@ import dayjs from 'dayjs';
|
||||
import { Dialog } from 'primeng/dialog';
|
||||
import { IPayRequestPayload, IPayResponse } from '../../models/invoices.io';
|
||||
import { SupplierInvoicesService } from '../../services';
|
||||
import { SupplierInvoicePayFormTemplateComponent } from './pay-form-template.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-supplier-invoice-pay-form',
|
||||
templateUrl: './pay-form.component.html',
|
||||
imports: [
|
||||
Dialog,
|
||||
ReactiveFormsModule,
|
||||
InventoryBankAccountSelectComponent,
|
||||
FormFooterActionsComponent,
|
||||
InputComponent,
|
||||
PaymentMethodTypeSelectComponent,
|
||||
],
|
||||
imports: [Dialog, ReactiveFormsModule, SupplierInvoicePayFormTemplateComponent],
|
||||
})
|
||||
export class SupplierInvoicePayFormComponent extends AbstractFormDialog<
|
||||
IPayResponse,
|
||||
|
||||
@@ -36,6 +36,15 @@ export const suppliersNamedRoutes: NamedRoutes<any> = {
|
||||
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;
|
||||
|
||||
export type TSuppliersRouteNames = (typeof suppliersNamedRoutes)[keyof typeof suppliersNamedRoutes];
|
||||
|
||||
@@ -20,6 +20,18 @@
|
||||
}
|
||||
</app-key-value>
|
||||
</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>
|
||||
<app-invoice-payments [supplierId]="supplierId" [invoiceId]="invoiceId" />
|
||||
@if (invoice() && supplier()) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { CatalogPurchaseReceiptStatusTagComponent } from '@/shared/catalog';
|
||||
import { KeyValueComponent } from '@/shared/components';
|
||||
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 { ActivatedRoute } from '@angular/router';
|
||||
import { ButtonDirective } from 'primeng/button';
|
||||
@@ -18,6 +20,8 @@ import { SupplierInvoiceStore, SupplierStore } from '../store';
|
||||
ButtonDirective,
|
||||
SupplierInvoicePayFormComponent,
|
||||
InvoicePaymentsComponent,
|
||||
PriceMaskDirective,
|
||||
PurchaseReceiptPaymentWrapperComponent,
|
||||
],
|
||||
})
|
||||
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 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 icon="pi pi-trash" severity="danger"></button>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { ToastService } from '@/core/services/toast.service';
|
||||
import { Component, effect, EventEmitter, inject, Input, Output, signal } from '@angular/core';
|
||||
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Subscribable } from 'rxjs';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { AbstractForm } from './abstract-form';
|
||||
|
||||
@Component({
|
||||
selector: 'abstract-form-dialog',
|
||||
template: '',
|
||||
imports: [ReactiveFormsModule],
|
||||
})
|
||||
export abstract class AbstractFormDialog<Response, Request> {
|
||||
@Input() initialValues?: Response;
|
||||
|
||||
export abstract class AbstractFormDialog<Response, Request> extends AbstractForm<
|
||||
Response,
|
||||
Request
|
||||
> {
|
||||
@Input()
|
||||
set visible(v: boolean) {
|
||||
this.visibleSignal.set(!!v);
|
||||
this.visibleChange.emit(!!v);
|
||||
}
|
||||
get visible() {
|
||||
return this.visibleSignal();
|
||||
@@ -21,58 +23,19 @@ export abstract class AbstractFormDialog<Response, Request> {
|
||||
private visibleSignal = signal(false);
|
||||
|
||||
@Output() visibleChange = new EventEmitter<boolean>();
|
||||
@Output() onSubmit = new EventEmitter<Response>();
|
||||
|
||||
protected readonly fb = inject(FormBuilder);
|
||||
constructor(private toastService: ToastService) {
|
||||
constructor() {
|
||||
super(inject(ToastService));
|
||||
effect(() => {
|
||||
const v = this.visibleSignal();
|
||||
if (!v) this.form.reset(this.defaultValues);
|
||||
});
|
||||
}
|
||||
|
||||
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.submit.bind(() => {
|
||||
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() {
|
||||
this.visibleChange.emit(false);
|
||||
override close() {
|
||||
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 class="grow flex flex-col gap-1">
|
||||
<span class="text-sm font-bold">{{ label }}</span>
|
||||
<ng-content>
|
||||
<span class="text-xl font-bold text-primary">{{ value }}</span>
|
||||
</ng-content>
|
||||
</div>
|
||||
</div>
|
||||
</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';
|
||||
|
||||
@Component({
|
||||
@@ -10,5 +11,7 @@ export class DetailsInfoCardComponent {
|
||||
@Input() icon: string = '';
|
||||
@Input() label: string = '';
|
||||
@Input() value: string = '';
|
||||
|
||||
@ViewChild('valueElement') valueElement: Maybe<ElementRef> = null;
|
||||
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