feat(purchases): implement purchase receipt functionality with form and receipt template

- Added `ProductPurchaseComponent` to handle product purchases.
- Created `PurchaseReceiptTemplateComponent` for displaying purchase receipt details.
- Developed `PurchaseFormComponent` for managing purchase form inputs and submission.
- Introduced `ProductChargeRowFormComponent` and `ProductChargeRowFormFieldsComponent` for handling product charge rows in the form.
- Implemented `PurchaseReceiptProductRowComponent` for displaying individual product rows in the receipt.
- Established API routes for purchase operations in `apiRoutes`.
- Integrated suppliers and inventories selection components.
- Added utility functions for converting prices to Persian alphabet.
- Created a utility for generating full names from name parts.
- Enhanced form validation and submission handling in the purchase form.
- Updated styles and templates for improved UI/UX.
This commit is contained in:
2025-12-09 20:17:00 +03:30
parent abf53bac03
commit 50bc9a4632
77 changed files with 1807 additions and 18834 deletions
@@ -8,7 +8,7 @@
<app-input label="شهر" [control]="form.controls.city" name="city" />
<app-input label="استان" [control]="form.controls.state" name="state" />
<app-input label="کشور" [control]="form.controls.country" name="country" />
<app-input label="فعال" [control]="form.controls.isActive" name="isActive" type="switch" />
<app-input label="وضعیت" [control]="form.controls.isActive" name="isActive" type="switch" />
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
</form>
</p-dialog>
@@ -41,13 +41,13 @@ export class SupplierFormComponent {
form = this.fb.group({
firstName: [this.initialValues?.firstName || null, [Validators.required]],
lastName: [this.initialValues?.lastName || null, [Validators.required]],
email: [this.initialValues?.email || null, [Validators.required, Validators.email]],
mobileNumber: [this.initialValues?.mobileNumber || null, [Validators.required]],
address: [this.initialValues?.address || null, [Validators.required]],
city: [this.initialValues?.city || null, [Validators.required]],
state: [this.initialValues?.state || null, [Validators.required]],
country: [this.initialValues?.country || null, [Validators.required]],
isActive: [this.initialValues?.isActive || false, [Validators.required]],
email: [this.initialValues?.email || null, [Validators.email]],
address: [this.initialValues?.address || null],
city: [this.initialValues?.city || null],
state: [this.initialValues?.state || null],
country: [this.initialValues?.country || null],
isActive: [this.initialValues?.isActive || false],
});
submitLoading = signal(false);
@@ -0,0 +1,15 @@
<div class="">
<uikit-field label="تامین‌کننده">
<p-select
[options]="items()"
optionLabel="fullname"
optionValue="id"
placeholder="انتخاب تامین‌کننده"
[formControl]="control"
[showClear]="true"
[filter]="true"
appendTo="body"
>
</p-select>
</uikit-field>
</div>
@@ -0,0 +1,47 @@
import { Maybe } from '@/core';
import { UikitFieldComponent } from '@/uikit';
import { Component, Input, signal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { Select } from 'primeng/select';
import { ISupplierResponse } from '../../models';
import { SuppliersService } from '../../services/main.service';
@Component({
selector: 'suppliers-select-field',
templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent],
})
export class SuppliersSelectComponent {
@Input() control!: FormControl<Maybe<number>>;
@Input() canInsert: boolean = false;
constructor(private service: SuppliersService) {
this.getData();
}
loading = signal(false);
items = signal<Maybe<ISupplierResponse[]>>(null);
isOpenFormDialog = signal(false);
getData() {
this.loading.set(true);
this.service.getAll().subscribe({
next: (res) => {
this.items.set(res.data);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
}
refresh() {
this.getData();
}
onOpenForm() {
// this.isOpenFormDialog.set(true);
}
}
+3 -1
View File
@@ -14,7 +14,9 @@ export interface ISupplierRawResponse {
deletedAt: Date;
}
export interface ISupplierResponse extends ISupplierRawResponse {}
export interface ISupplierResponse extends ISupplierRawResponse {
fullname: string;
}
export interface ISupplierRequest {
firstName: string;
@@ -1,7 +1,7 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map, Observable } from 'rxjs';
import { SUPPLIERS_API_ROUTES } from '../constants';
import { ISupplierRawResponse, ISupplierRequest, ISupplierResponse } from '../models';
@@ -12,14 +12,32 @@ export class SuppliersService {
private apiRoutes = SUPPLIERS_API_ROUTES;
getAll(): Observable<IPaginatedResponse<ISupplierResponse>> {
return this.http.get<IPaginatedResponse<ISupplierRawResponse>>(this.apiRoutes.list());
return this.http.get<IPaginatedResponse<ISupplierRawResponse>>(this.apiRoutes.list()).pipe(
map((res) => ({
...res,
data: res.data.map((item) => ({
...item,
fullname: `${item.firstName} ${item.lastName}`,
})),
})),
);
}
getSingle(supplierId: string): Observable<ISupplierResponse> {
return this.http.get<ISupplierRawResponse>(this.apiRoutes.single(supplierId));
return this.http.get<ISupplierRawResponse>(this.apiRoutes.single(supplierId)).pipe(
map((item) => ({
...item,
fullname: `${item.firstName} ${item.lastName}`,
})),
);
}
create(data: ISupplierRequest): Observable<ISupplierResponse> {
return this.http.post<ISupplierRawResponse>(this.apiRoutes.list(), data);
return this.http.post<ISupplierRawResponse>(this.apiRoutes.list(), data).pipe(
map((item) => ({
...item,
fullname: `${item.firstName} ${item.lastName}`,
})),
);
}
}
@@ -18,18 +18,24 @@ export class SuppliersComponent {
}
columns = [
{ field: 'id', header: 'شناسه' },
{ field: 'firstName', header: 'نام' },
{ field: 'lastName', header: 'نام خانوادگی' },
{ field: 'email', header: 'ایمیل' },
{
field: 'name',
header: 'نام',
customDataModel(item) {
return `${item.firstName} ${item.lastName}` || '-';
},
},
{ field: 'mobileNumber', header: 'شماره موبایل' },
{ field: 'address', header: 'آدرس' },
{ field: 'city', header: 'شهر' },
{ field: 'state', header: 'استان' },
{ field: 'country', header: 'کشور' },
{ field: 'isActive', header: 'فعال' },
{ field: 'createdAt', header: 'تاریخ ایجاد' },
{ field: 'updatedAt', header: 'تاریخ به‌روزرسانی' },
{
field: 'isActive',
header: 'وضعیت',
customDataModel(item) {
return item.isActive ? 'فعال' : 'غیرفعال';
},
},
{ field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' },
{ field: 'updatedAt', header: 'تاریخ به‌روزرسانی', type: 'date' },
] as IColumn[];
loading = signal(false);