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,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);
|
||||
});
|
||||
this.submit.bind(() => {
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
|
||||
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.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>
|
||||
<span class="text-xl font-bold text-primary">{{ value }}</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