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:
Generated
-18772
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@
|
|||||||
<app-input label="شهر" [control]="form.controls.city" name="city" />
|
<app-input label="شهر" [control]="form.controls.city" name="city" />
|
||||||
<app-input label="استان" [control]="form.controls.state" name="state" />
|
<app-input label="استان" [control]="form.controls.state" name="state" />
|
||||||
<app-input label="کشور" [control]="form.controls.country" name="country" />
|
<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()" />
|
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
|
||||||
</form>
|
</form>
|
||||||
</p-dialog>
|
</p-dialog>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<div class="">
|
||||||
|
<uikit-field label="مشتری">
|
||||||
|
<p-select
|
||||||
|
[options]="brands()"
|
||||||
|
optionLabel="name"
|
||||||
|
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 { ICustomerResponse } from '../../models';
|
||||||
|
import { CustomersService } from '../../services/main.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'customers-select-field',
|
||||||
|
templateUrl: './select.component.html',
|
||||||
|
imports: [ReactiveFormsModule, Select, UikitFieldComponent],
|
||||||
|
})
|
||||||
|
export class CustomersSelectComponent {
|
||||||
|
@Input() control!: FormControl<Maybe<number>>;
|
||||||
|
@Input() canInsert: boolean = false;
|
||||||
|
|
||||||
|
constructor(private service: CustomersService) {
|
||||||
|
this.getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = signal(false);
|
||||||
|
brands = signal<Maybe<ICustomerResponse[]>>(null);
|
||||||
|
|
||||||
|
isOpenFormDialog = signal(false);
|
||||||
|
|
||||||
|
getData() {
|
||||||
|
this.loading.set(true);
|
||||||
|
this.service.getAll().subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.brands.set(res.data);
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpenForm() {
|
||||||
|
// this.isOpenFormDialog.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,8 +28,9 @@ export class CustomersComponent {
|
|||||||
{ field: 'state', header: 'استان' },
|
{ field: 'state', header: 'استان' },
|
||||||
{ field: 'country', header: 'کشور' },
|
{ field: 'country', header: 'کشور' },
|
||||||
{ field: 'isActive', header: 'فعال' },
|
{ field: 'isActive', header: 'فعال' },
|
||||||
{ field: 'createdAt', header: 'تاریخ ایجاد' },
|
{ field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' },
|
||||||
{ field: 'updatedAt', header: 'تاریخ بهروزرسانی' },
|
{ field: 'updatedAt', header: 'تاریخ بهروزرسانی', type: 'date' },
|
||||||
|
,
|
||||||
] as IColumn[];
|
] as IColumn[];
|
||||||
|
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<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.name" name="name" />
|
<app-input label="نام" [control]="form.controls.name" name="name" />
|
||||||
<app-input label="موقعیت" [control]="form.controls.location" name="location" />
|
<app-input label="موقعیت" [control]="form.controls.location" name="location" />
|
||||||
<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()" />
|
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
|
||||||
</form>
|
</form>
|
||||||
</p-dialog>
|
</p-dialog>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<div class="">
|
||||||
|
<uikit-field label="انبار">
|
||||||
|
<p-select
|
||||||
|
[options]="brands()"
|
||||||
|
optionLabel="name"
|
||||||
|
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 { IInventoryResponse } from '../../models';
|
||||||
|
import { InventoriesService } from '../../services/main.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'inventories-select-field',
|
||||||
|
templateUrl: './select.component.html',
|
||||||
|
imports: [ReactiveFormsModule, Select, UikitFieldComponent],
|
||||||
|
})
|
||||||
|
export class InventoriesSelectComponent {
|
||||||
|
@Input() control!: FormControl<Maybe<number>>;
|
||||||
|
@Input() canInsert: boolean = false;
|
||||||
|
|
||||||
|
constructor(private service: InventoriesService) {
|
||||||
|
this.getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = signal(false);
|
||||||
|
brands = signal<Maybe<IInventoryResponse[]>>(null);
|
||||||
|
|
||||||
|
isOpenFormDialog = signal(false);
|
||||||
|
|
||||||
|
getData() {
|
||||||
|
this.loading.set(true);
|
||||||
|
this.service.getAll().subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.brands.set(res.data);
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpenForm() {
|
||||||
|
// this.isOpenFormDialog.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,80 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Maybe } from '@/core';
|
||||||
|
import { ProductChargeFormComponent } from '@/modules/productCharges/components/form.component';
|
||||||
|
import { KeyValueComponent } from '@/shared/components';
|
||||||
|
import { Component, inject, signal } from '@angular/core';
|
||||||
|
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||||
|
import { Button, ButtonDirective } from 'primeng/button';
|
||||||
|
import { Card } from 'primeng/card';
|
||||||
|
import { GalleriaModule } from 'primeng/galleria';
|
||||||
|
import { IInventoryResponse } from '../models';
|
||||||
|
import { InventoriesService } from '../services/main.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-inventory',
|
selector: 'app-product',
|
||||||
templateUrl: './single.component.html',
|
templateUrl: './single.component.html',
|
||||||
|
imports: [
|
||||||
|
ButtonDirective,
|
||||||
|
RouterLink,
|
||||||
|
GalleriaModule,
|
||||||
|
KeyValueComponent,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
ProductChargeFormComponent,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class InventoryComponent {
|
export class InventoryComponent {
|
||||||
constructor() {}
|
private route = inject(ActivatedRoute);
|
||||||
|
constructor(private service: InventoriesService) {
|
||||||
|
this.getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
productId = this.route.snapshot.params['productId'];
|
||||||
|
isOpenChargeForm = signal(false);
|
||||||
|
loading = signal(false);
|
||||||
|
data = signal<Maybe<IInventoryResponse>>(null);
|
||||||
|
|
||||||
|
getData() {
|
||||||
|
this.loading.set(true);
|
||||||
|
this.service.getSingle(this.productId).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.data.set(res);
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get topBarCardDetails() {
|
||||||
|
return [
|
||||||
|
// {
|
||||||
|
// icon: 'pi pi-box',
|
||||||
|
// label: 'موجودی',
|
||||||
|
// value: this.data()? || 0,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// icon: 'pi pi-dollar',
|
||||||
|
// label: 'قیمت پایه',
|
||||||
|
// value: this.data()?.basePrice?.toString() || 'تعیین نشده',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// icon: 'pi pi-calendar',
|
||||||
|
// label: 'تاریخ ایجاد',
|
||||||
|
// value: this.data()?.createdAt
|
||||||
|
// ? new Date(this.data()!.createdAt!).toLocaleDateString()
|
||||||
|
// : '-',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// icon: 'pi pi-clock',
|
||||||
|
// label: 'تعداد فروش',
|
||||||
|
// value: 1200,
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
openDeleteConfirm() {}
|
||||||
|
openChargeForm() {
|
||||||
|
this.isOpenChargeForm.set(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
[pageTitle]="'مدیریت برندهای محصول'"
|
[pageTitle]="'مدیریت برندهای محصول'"
|
||||||
[addNewCtaLabel]="'افزودن برند جدید'"
|
[addNewCtaLabel]="'افزودن برند جدید'"
|
||||||
[columns]="columns"
|
[columns]="columns"
|
||||||
[showAdd]="true"
|
|
||||||
emptyPlaceholderTitle="برندی یافت نشد"
|
|
||||||
emptyPlaceholderDescription="برای افزودن برند جدید، روی دکمهٔ بالا کلیک کنید."
|
|
||||||
[items]="items()"
|
[items]="items()"
|
||||||
[loading]="loading()"
|
[loading]="loading()"
|
||||||
|
emptyPlaceholderTitle="برندی یافت نشد"
|
||||||
|
emptyPlaceholderDescription="برای افزودن برند جدید، روی دکمهٔ بالا کلیک کنید."
|
||||||
|
[showAdd]="true"
|
||||||
[showDetails]="true"
|
[showDetails]="true"
|
||||||
(onAdd)="openAddForm()"
|
(onAdd)="openAddForm()"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ export class ProductBrandsComponent {
|
|||||||
{ field: 'name', header: 'نام' },
|
{ field: 'name', header: 'نام' },
|
||||||
{ field: 'description', header: 'توضیحات' },
|
{ field: 'description', header: 'توضیحات' },
|
||||||
{ field: 'imageUrl', header: 'تصویر' },
|
{ field: 'imageUrl', header: 'تصویر' },
|
||||||
{ field: 'createdAt', header: 'تاریخ ایجاد' },
|
{ field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' },
|
||||||
{ field: 'updatedAt', header: 'تاریخ بهروزرسانی' },
|
{ field: 'updatedAt', header: 'تاریخ بهروزرسانی', type: 'date' },
|
||||||
] as IColumn[];
|
] as IColumn[];
|
||||||
|
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
@@ -37,6 +37,8 @@ export class ProductBrandsComponent {
|
|||||||
getData() {
|
getData() {
|
||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
this.productBrandService.getAll().subscribe((res) => {
|
this.productBrandService.getAll().subscribe((res) => {
|
||||||
|
console.log(res);
|
||||||
|
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
this.items.set(res.data);
|
this.items.set(res.data);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export class ProductCategoriesComponent {
|
|||||||
{ field: 'name', header: 'نام' },
|
{ field: 'name', header: 'نام' },
|
||||||
{ field: 'description', header: 'توضیحات' },
|
{ field: 'description', header: 'توضیحات' },
|
||||||
{ field: 'imageUrl', header: 'تصویر' },
|
{ field: 'imageUrl', header: 'تصویر' },
|
||||||
{ field: 'createdAt', header: 'تاریخ ایجاد' },
|
{ field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' },
|
||||||
] as IColumn[];
|
] as IColumn[];
|
||||||
|
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<p-dialog [(visible)]="visible" [modal]="true" [style]="{ width: '50vw' }" header="افزودن هزینه محصول">
|
||||||
|
<form [formGroup]="form" (ngSubmit)="submit()">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<products-select-field [control]="form.controls.productId" />
|
||||||
|
|
||||||
|
<inventories-select-field [control]="form.controls.inventoryId" />
|
||||||
|
|
||||||
|
<suppliers-select-field [control]="form.controls.supplierId" />
|
||||||
|
<app-input label="تعداد" [control]="form.controls.count" />
|
||||||
|
|
||||||
|
<app-input label="قیمت اولیه" [control]="form.controls.fee" />
|
||||||
|
|
||||||
|
<app-input label="مبلغ کل" [control]="form.controls.totalAmount" />
|
||||||
|
|
||||||
|
<app-input label="تاریخ خرید" [control]="form.controls.buyAt" />
|
||||||
|
|
||||||
|
<div class="col-span-2">
|
||||||
|
<textarea pTextarea label="توضیحات" [formControl]="form.controls.description"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-input label="تسویه شده" type="switch" [control]="form.controls.isSettled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-form-footer-actions (onSubmit)="submit()" (onCancel)="cancel()" />
|
||||||
|
</form>
|
||||||
|
</p-dialog>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { InventoriesSelectComponent } from '@/modules/inventories/components/select/select.component';
|
||||||
|
import { ProductsSelectComponent } from '@/modules/products/components/select/select.component';
|
||||||
|
import { SuppliersSelectComponent } from '@/modules/suppliers/components/select/select.component';
|
||||||
|
import { InputComponent } from '@/shared/components';
|
||||||
|
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
||||||
|
import { Component, effect, inject, model, output } from '@angular/core';
|
||||||
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { DialogModule } from 'primeng/dialog';
|
||||||
|
import { Textarea } from 'primeng/textarea';
|
||||||
|
import { IProductChargeRequest } from '../models';
|
||||||
|
import { ProductChargesService } from '../services/main.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'product-charge-form',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
DialogModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
InputComponent,
|
||||||
|
FormFooterActionsComponent,
|
||||||
|
Textarea,
|
||||||
|
ProductsSelectComponent,
|
||||||
|
InventoriesSelectComponent,
|
||||||
|
SuppliersSelectComponent,
|
||||||
|
],
|
||||||
|
templateUrl: './form.component.html',
|
||||||
|
})
|
||||||
|
export class ProductChargeFormComponent {
|
||||||
|
private fb = inject(FormBuilder);
|
||||||
|
private productChargeService = inject(ProductChargesService);
|
||||||
|
|
||||||
|
visible = model<boolean>(false);
|
||||||
|
onSubmit = output();
|
||||||
|
|
||||||
|
form = this.fb.group({
|
||||||
|
count: [0, [Validators.required, Validators.min(1)]],
|
||||||
|
fee: [0, [Validators.required, Validators.min(0)]],
|
||||||
|
totalAmount: [0, [Validators.required, Validators.min(0)]],
|
||||||
|
isSettled: [false],
|
||||||
|
buyAt: ['', Validators.required],
|
||||||
|
description: [''],
|
||||||
|
productId: [0, [Validators.required, Validators.min(1)]],
|
||||||
|
inventoryId: [0, [Validators.required, Validators.min(1)]],
|
||||||
|
supplierId: [0, [Validators.required, Validators.min(1)]],
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
effect(() => {
|
||||||
|
if (!this.visible()) {
|
||||||
|
this.form.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
this.form.markAllAsTouched();
|
||||||
|
if (this.form.valid) {
|
||||||
|
this.form.disable();
|
||||||
|
const data = this.form.value as IProductChargeRequest;
|
||||||
|
|
||||||
|
this.productChargeService.create(data).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.form.enable();
|
||||||
|
this.visible.set(false);
|
||||||
|
this.onSubmit.emit();
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.form.enable();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.visible.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const BASE_URL = '/api/v1/product-charges';
|
||||||
|
|
||||||
|
export const PRODUCT_CHARGES_API_ROUTES = {
|
||||||
|
getAll: BASE_URL,
|
||||||
|
getSingle: (id: number) => `${BASE_URL}/${id}`,
|
||||||
|
create: BASE_URL,
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './apiRoutes';
|
||||||
|
export * from './routes';
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
|
export type TProductChargesRouteNames = 'product-charges' | 'product-charge';
|
||||||
|
|
||||||
|
export const PRODUCT_CHARGES_ROUTES: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('../../views/list.component').then((m) => m.ProductChargesComponent),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':productChargeId',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('../../views/single.component').then((m) => m.ProductChargeComponent),
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './io';
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
export interface IProductChargeRaw {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
fee: number;
|
||||||
|
totalAmount: number;
|
||||||
|
isSettled?: boolean;
|
||||||
|
buyAt: string;
|
||||||
|
description?: string;
|
||||||
|
productId: number;
|
||||||
|
inventoryId: number;
|
||||||
|
supplierId: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProductChargeResponse extends IProductChargeRaw {}
|
||||||
|
|
||||||
|
export interface IProductChargeRequest {
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
fee: number;
|
||||||
|
totalAmount: number;
|
||||||
|
isSettled?: boolean;
|
||||||
|
buyAt: string;
|
||||||
|
description?: string;
|
||||||
|
productId: number;
|
||||||
|
inventoryId: number;
|
||||||
|
supplierId: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { inject, Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { PRODUCT_CHARGES_API_ROUTES } from '../constants';
|
||||||
|
import { IProductChargeRequest, IProductChargeResponse } from '../models';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class ProductChargesService {
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
|
||||||
|
getAll(): Observable<{ data: IProductChargeResponse[] }> {
|
||||||
|
return this.http.get<{ data: IProductChargeResponse[] }>(PRODUCT_CHARGES_API_ROUTES.getAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSingle(id: number): Observable<{ data: IProductChargeResponse }> {
|
||||||
|
return this.http.get<{ data: IProductChargeResponse }>(
|
||||||
|
PRODUCT_CHARGES_API_ROUTES.getSingle(id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(data: IProductChargeRequest): Observable<{ data: IProductChargeResponse }> {
|
||||||
|
return this.http.post<{ data: IProductChargeResponse }>(
|
||||||
|
PRODUCT_CHARGES_API_ROUTES.create,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<app-page-data-list [items]="items()" [columns]="columns" [loading]="loading()" (onAdd)="openAddForm()" />
|
||||||
|
|
||||||
|
<product-charge-form [(visible)]="visibleForm" (onSubmit)="refresh()" />
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import {
|
||||||
|
IColumn,
|
||||||
|
PageDataListComponent,
|
||||||
|
} from '@/shared/components/pageDataList/page-data-list.component';
|
||||||
|
import { Component, effect, inject, signal } from '@angular/core';
|
||||||
|
import { ProductChargeFormComponent } from '../components/form.component';
|
||||||
|
import { IProductChargeResponse } from '../models';
|
||||||
|
import { ProductChargesService } from '../services/main.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-product-charges',
|
||||||
|
standalone: true,
|
||||||
|
imports: [PageDataListComponent, ProductChargeFormComponent],
|
||||||
|
templateUrl: './list.component.html',
|
||||||
|
})
|
||||||
|
export class ProductChargesComponent {
|
||||||
|
private productChargeService = inject(ProductChargesService);
|
||||||
|
|
||||||
|
loading = signal(false);
|
||||||
|
items = signal<IProductChargeResponse[]>([]);
|
||||||
|
visibleForm = signal(false);
|
||||||
|
|
||||||
|
columns: IColumn<IProductChargeResponse>[] = [
|
||||||
|
{ field: 'id', header: 'شناسه' },
|
||||||
|
{ field: 'name', header: 'نام' },
|
||||||
|
{ field: 'count', header: 'تعداد' },
|
||||||
|
{ field: 'fee', header: 'هزینه' },
|
||||||
|
{ field: 'totalAmount', header: 'مبلغ کل' },
|
||||||
|
{ field: 'isSettled', header: 'تسویه شده' },
|
||||||
|
{ field: 'buyAt', header: 'تاریخ خرید' },
|
||||||
|
{ field: 'productId', header: 'شناسه محصول' },
|
||||||
|
{ field: 'inventoryId', header: 'شناسه انبار' },
|
||||||
|
{ field: 'supplierId', header: 'شناسه تامینکننده' },
|
||||||
|
{ field: 'createdAt', header: 'تاریخ ایجاد' },
|
||||||
|
{ field: 'updatedAt', header: 'تاریخ بروزرسانی' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
effect(() => {
|
||||||
|
this.getData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
openAddForm() {
|
||||||
|
this.visibleForm.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
getData() {
|
||||||
|
this.loading.set(true);
|
||||||
|
this.productChargeService.getAll().subscribe((res) => {
|
||||||
|
this.loading.set(false);
|
||||||
|
this.items.set(res.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<p>product-charge works!</p>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-product-charge',
|
||||||
|
standalone: true,
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './single.component.html',
|
||||||
|
})
|
||||||
|
export class ProductChargeComponent {}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<uikit-field label="محصول" [control]="control" [showLabel]="showLabel" [showErrors]="showErrors">
|
||||||
|
<p-select
|
||||||
|
[options]="items()"
|
||||||
|
optionLabel="name"
|
||||||
|
[optionValue]="selectOptionValue"
|
||||||
|
placeholder="انتخاب محصول"
|
||||||
|
[formControl]="control"
|
||||||
|
[showClear]="true"
|
||||||
|
[filter]="true"
|
||||||
|
appendTo="body"
|
||||||
|
>
|
||||||
|
</p-select>
|
||||||
|
</uikit-field>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Maybe } from '@/core';
|
||||||
|
import { UikitFieldComponent } from '@/uikit';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, Input, signal } from '@angular/core';
|
||||||
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { Select } from 'primeng/select';
|
||||||
|
import { IProductResponse } from '../../models';
|
||||||
|
import { ProductsService } from '../../services/main.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'products-select-field',
|
||||||
|
templateUrl: './select.component.html',
|
||||||
|
imports: [ReactiveFormsModule, Select, UikitFieldComponent, CommonModule],
|
||||||
|
})
|
||||||
|
export class ProductsSelectComponent {
|
||||||
|
@Input() control!: FormControl<Maybe<number | IProductResponse>>;
|
||||||
|
@Input() canInsert: boolean = false;
|
||||||
|
@Input() showLabel: boolean = true;
|
||||||
|
@Input() showErrors: boolean = true;
|
||||||
|
|
||||||
|
constructor(private service: ProductsService) {
|
||||||
|
this.getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = signal(false);
|
||||||
|
items = signal<Maybe<IProductResponse[]>>(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectOptionValue() {
|
||||||
|
if (typeof this.control.value === 'number') {
|
||||||
|
return 'id';
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NamedRoutes } from '@/core';
|
import { NamedRoutes } from '@/core';
|
||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
export type TProductsRouteNames = 'products' | 'product' | 'create' | 'update';
|
export type TProductsRouteNames = 'products' | 'product' | 'create' | 'update' | 'purchase';
|
||||||
|
|
||||||
export const productsNamedRoutes: NamedRoutes<TProductsRouteNames> = {
|
export const productsNamedRoutes: NamedRoutes<TProductsRouteNames> = {
|
||||||
products: {
|
products: {
|
||||||
@@ -38,6 +38,15 @@ export const productsNamedRoutes: NamedRoutes<TProductsRouteNames> = {
|
|||||||
pagePath: () => '/products/:productId/update',
|
pagePath: () => '/products/:productId/update',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
purchase: {
|
||||||
|
path: 'products/:productId/purchase',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('../../views/purchase.component').then((m) => m.ProductPurchaseComponent),
|
||||||
|
meta: {
|
||||||
|
title: 'محصول',
|
||||||
|
pagePath: () => '/products/:productId/purchase',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PRODUCTS_ROUTES: Routes = Object.values(productsNamedRoutes);
|
export const PRODUCTS_ROUTES: Routes = Object.values(productsNamedRoutes);
|
||||||
|
|||||||
+1
@@ -12,6 +12,7 @@ export interface IProductRawResponse {
|
|||||||
category?: IProductCategory;
|
category?: IProductCategory;
|
||||||
// supplierId: number;
|
// supplierId: number;
|
||||||
basePrice?: string;
|
basePrice?: string;
|
||||||
|
count?: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
deletedAt?: Maybe<Date>;
|
deletedAt?: Maybe<Date>;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './create.component';
|
export * from './create.component';
|
||||||
export * from './list.component';
|
export * from './list.component';
|
||||||
|
export * from './purchase.component';
|
||||||
export * from './single.component';
|
export * from './single.component';
|
||||||
export * from './update.component';
|
export * from './update.component';
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<purchase-receipt-template />
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { PurchaseReceiptTemplateComponent } from '@/modules/purchases/components/purchase-receipt-template.component';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'product-purchase',
|
||||||
|
templateUrl: './purchase.component.html',
|
||||||
|
imports: [PurchaseReceiptTemplateComponent],
|
||||||
|
})
|
||||||
|
export class ProductPurchaseComponent {
|
||||||
|
constructor() {}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
</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" (click)="openChargeForm()"></button>
|
||||||
<button
|
<button
|
||||||
pButton
|
pButton
|
||||||
type="button"
|
type="button"
|
||||||
@@ -51,14 +52,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="shrink-0 w-xs">
|
<div class="shrink-0 w-xs">
|
||||||
<p-card class="border border-primary-700">
|
<p-card class="border border-primary-700">
|
||||||
<div class="flex justify-between p-2 gap-2">
|
<div class="flex justify-between items-center p-2 gap-2">
|
||||||
<span class="font-bold text-lg">دستهبندی</span>
|
<span class="">دستهبندی</span>
|
||||||
<p class="m-0">{{ product()?.category?.name || "بدون دستهبندی" }}</p>
|
<p class="m-0 font-bold text-lg">{{ product()?.category?.name || "بدون دستهبندی" }}</p>
|
||||||
</div>
|
</div>
|
||||||
<hr class="m-0" />
|
<hr class="m-0" />
|
||||||
<div class="flex justify-between p-2 gap-2">
|
<div class="flex justify-between items-center p-2 gap-2">
|
||||||
<span class="font-bold text-lg">برند</span>
|
<span class="">برند</span>
|
||||||
<p class="m-0">{{ product()?.brand?.name || "بدون برند" }}</p>
|
<p class="m-0 font-bold text-lg">{{ product()?.brand?.name || "بدون برند" }}</p>
|
||||||
</div>
|
</div>
|
||||||
</p-card>
|
</p-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,4 +82,5 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</p-galleria>
|
</p-galleria>
|
||||||
</div>
|
</div>
|
||||||
|
<purchase-form [(visible)]="isOpenChargeForm" [product]="product()!" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Maybe } from '@/core';
|
import { Maybe } from '@/core';
|
||||||
|
import { PurchaseFormComponent } from '@/modules/purchases/components/form.component';
|
||||||
import { KeyValueComponent } from '@/shared/components';
|
import { KeyValueComponent } from '@/shared/components';
|
||||||
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';
|
||||||
@@ -11,7 +12,15 @@ import { ProductsService } from '../services/main.service';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-product',
|
selector: 'app-product',
|
||||||
templateUrl: './single.component.html',
|
templateUrl: './single.component.html',
|
||||||
imports: [ButtonDirective, RouterLink, GalleriaModule, KeyValueComponent, Card, Button],
|
imports: [
|
||||||
|
ButtonDirective,
|
||||||
|
RouterLink,
|
||||||
|
GalleriaModule,
|
||||||
|
KeyValueComponent,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
PurchaseFormComponent,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class ProductComponent {
|
export class ProductComponent {
|
||||||
private route = inject(ActivatedRoute);
|
private route = inject(ActivatedRoute);
|
||||||
@@ -48,6 +57,7 @@ export class ProductComponent {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
productId = this.route.snapshot.params['productId'];
|
productId = this.route.snapshot.params['productId'];
|
||||||
|
isOpenChargeForm = signal(false);
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
product = signal<Maybe<IProductResponse>>(null);
|
product = signal<Maybe<IProductResponse>>(null);
|
||||||
|
|
||||||
@@ -69,7 +79,7 @@ export class ProductComponent {
|
|||||||
{
|
{
|
||||||
icon: 'pi pi-box',
|
icon: 'pi pi-box',
|
||||||
label: 'موجودی',
|
label: 'موجودی',
|
||||||
value: 0,
|
value: this.product()?.count || 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'pi pi-dollar',
|
icon: 'pi pi-dollar',
|
||||||
@@ -92,4 +102,7 @@ export class ProductComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openDeleteConfirm() {}
|
openDeleteConfirm() {}
|
||||||
|
openChargeForm() {
|
||||||
|
this.isOpenChargeForm.set(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<p-dialog [(visible)]="visible" [modal]="true" [style]="{ width: '50vw' }" [header]="formTitle">
|
||||||
|
<form [formGroup]="form" (ngSubmit)="submit()">
|
||||||
|
<div class="grid grid-cols-1">
|
||||||
|
<product-charge-row-form [productsControl]="form.controls.products" [isSingleProduct]="!!product" />
|
||||||
|
<hr />
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<inventories-select-field [control]="form.controls.inventoryId" />
|
||||||
|
|
||||||
|
<suppliers-select-field [control]="form.controls.supplierId" />
|
||||||
|
|
||||||
|
<app-input label="مبلغ کل" [control]="form.controls.totalAmount" />
|
||||||
|
|
||||||
|
<app-input label="تاریخ خرید" [control]="form.controls.buyAt" />
|
||||||
|
|
||||||
|
<div class="col-span-2">
|
||||||
|
<textarea pTextarea label="توضیحات" [formControl]="form.controls.description"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-input label="تسویه شده" type="switch" [control]="form.controls.isSettled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-form-footer-actions (onSubmit)="submit()" (onCancel)="cancel()" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</p-dialog>
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { InventoriesSelectComponent } from '@/modules/inventories/components/select/select.component';
|
||||||
|
import { IInventoryResponse } from '@/modules/inventories/models';
|
||||||
|
import { IProductResponse } from '@/modules/products/models';
|
||||||
|
import { SuppliersSelectComponent } from '@/modules/suppliers/components/select/select.component';
|
||||||
|
import { ISupplierResponse } from '@/modules/suppliers/models';
|
||||||
|
import { InputComponent } from '@/shared/components';
|
||||||
|
import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component';
|
||||||
|
import { Component, effect, inject, Input, model, output } from '@angular/core';
|
||||||
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { DialogModule } from 'primeng/dialog';
|
||||||
|
import { Textarea } from 'primeng/textarea';
|
||||||
|
import { IPurchaseRequest } from '../models';
|
||||||
|
import { PurchasesService } from '../services/main.service';
|
||||||
|
import { ProductChargeRowFormComponent } from './product-charge-row-form.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'purchase-form',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
DialogModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
InputComponent,
|
||||||
|
FormFooterActionsComponent,
|
||||||
|
Textarea,
|
||||||
|
InventoriesSelectComponent,
|
||||||
|
SuppliersSelectComponent,
|
||||||
|
ProductChargeRowFormComponent,
|
||||||
|
],
|
||||||
|
templateUrl: './form.component.html',
|
||||||
|
})
|
||||||
|
export class PurchaseFormComponent {
|
||||||
|
@Input() product?: IProductResponse;
|
||||||
|
@Input() inventory?: IInventoryResponse;
|
||||||
|
@Input() supplier?: ISupplierResponse;
|
||||||
|
|
||||||
|
private fb = inject(FormBuilder);
|
||||||
|
private service = inject(PurchasesService);
|
||||||
|
|
||||||
|
visible = model<boolean>(false);
|
||||||
|
onSubmit = output();
|
||||||
|
|
||||||
|
form = this.fb.group({
|
||||||
|
totalAmount: [0, [Validators.required, Validators.min(0)]],
|
||||||
|
isSettled: [false],
|
||||||
|
buyAt: ['', Validators.required],
|
||||||
|
description: [''],
|
||||||
|
products: this.fb.array([
|
||||||
|
this.fb.group({
|
||||||
|
productId: [this.product?.id || 0, [Validators.required]],
|
||||||
|
count: [0, [Validators.required, Validators.min(1)]],
|
||||||
|
fee: [0, [Validators.required, Validators.min(0)]],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
inventoryId: [this.inventory?.id || 0, [Validators.required]],
|
||||||
|
supplierId: [this.supplier?.id || 0, [Validators.required]],
|
||||||
|
});
|
||||||
|
|
||||||
|
get formTitle() {
|
||||||
|
if (this.product) {
|
||||||
|
return `شارژ محصول ${this.product.name}`;
|
||||||
|
}
|
||||||
|
if (this.inventory) {
|
||||||
|
return `شارژ انبار ${this.inventory.name}`;
|
||||||
|
}
|
||||||
|
if (this.supplier) {
|
||||||
|
return `شارژ تامینکننده ${this.supplier.fullname}`;
|
||||||
|
}
|
||||||
|
return 'شارژ محصول';
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
effect(() => {
|
||||||
|
if (!this.visible()) {
|
||||||
|
this.form.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
this.form.markAllAsTouched();
|
||||||
|
console.log(this.form);
|
||||||
|
console.log(this.form.value);
|
||||||
|
|
||||||
|
if (this.form.valid) {
|
||||||
|
this.form.disable();
|
||||||
|
const data = this.form.value as IPurchaseRequest;
|
||||||
|
|
||||||
|
this.service.create(data).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.form.enable();
|
||||||
|
this.visible.set(false);
|
||||||
|
this.onSubmit.emit();
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.form.enable();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.visible.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<products-select-field [control]="productIdControl" />
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<app-input label="تعداد" [control]="countControl" name="count" />
|
||||||
|
<app-input label="قیمت" [control]="feeControl" name="fee" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Maybe } from '@/core';
|
||||||
|
import { ProductsSelectComponent } from '@/modules/products/components/select/select.component';
|
||||||
|
import { InputComponent } from '@/shared/components';
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { FormControl } from '@angular/forms';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'product-charge-row-form-fields',
|
||||||
|
templateUrl: './product-charge-row-form-fields.component.html',
|
||||||
|
imports: [ProductsSelectComponent, InputComponent],
|
||||||
|
})
|
||||||
|
export class ProductChargeRowFormFieldsComponent {
|
||||||
|
@Input() productIdControl!: FormControl<Maybe<number>>;
|
||||||
|
@Input() countControl!: FormControl<Maybe<number>>;
|
||||||
|
@Input() feeControl!: FormControl<Maybe<number>>;
|
||||||
|
constructor() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="space-y-3">
|
||||||
|
@for (product of products; let i = $index; track product.controls["productId"].value) {
|
||||||
|
<div class="flex items-center gap-3 p-3 border border-surface-700 rounded-lg">
|
||||||
|
<div class="flex-1">
|
||||||
|
<product-charge-row-form-fields
|
||||||
|
[productIdControl]="$any(product.get('productId'))"
|
||||||
|
[countControl]="$any(product.get('count'))"
|
||||||
|
[feeControl]="$any(product.get('fee'))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (canAddOrRemoveProducts) {
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
type="button"
|
||||||
|
icon="pi pi-trash"
|
||||||
|
severity="danger"
|
||||||
|
size="small"
|
||||||
|
class="shrink-0"
|
||||||
|
(click)="removeProduct(i)"
|
||||||
|
[disabled]="productsControl.length === 1"
|
||||||
|
pTooltip="حذف محصول"
|
||||||
|
></button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (canAddOrRemoveProducts) {
|
||||||
|
<button pButton type="button" label="افزودن محصول" (click)="addProduct()" pTooltip="افزودن محصول"></button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Maybe } from '@/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, Input, OnInit, inject } from '@angular/core';
|
||||||
|
import {
|
||||||
|
FormArray,
|
||||||
|
FormBuilder,
|
||||||
|
FormControl,
|
||||||
|
FormGroup,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
Validators,
|
||||||
|
} from '@angular/forms';
|
||||||
|
import { ButtonModule } from 'primeng/button';
|
||||||
|
import { ProductChargeRowFormFieldsComponent } from './product-charge-row-form-fields.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'product-charge-row-form',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, ReactiveFormsModule, ButtonModule, ProductChargeRowFormFieldsComponent],
|
||||||
|
templateUrl: './product-charge-row-form.component.html',
|
||||||
|
})
|
||||||
|
export class ProductChargeRowFormComponent implements OnInit {
|
||||||
|
private fb = inject(FormBuilder);
|
||||||
|
|
||||||
|
@Input() productsControl!: FormArray<
|
||||||
|
FormGroup<{
|
||||||
|
productId: FormControl<Maybe<number>>;
|
||||||
|
count: FormControl<Maybe<number>>;
|
||||||
|
fee: FormControl<Maybe<number>>;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
@Input() isSingleProduct = false;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (this.productsControl.length === 0) {
|
||||||
|
this.addProduct();
|
||||||
|
}
|
||||||
|
if (this.isSingleProduct) {
|
||||||
|
this.productsControl.at(0).get('productId')?.disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addProduct() {
|
||||||
|
const productFormGroup = this.fb.group({
|
||||||
|
productId: [0, [Validators.required]],
|
||||||
|
count: [1, [Validators.required, Validators.min(1)]],
|
||||||
|
fee: [0, [Validators.required, Validators.min(0)]],
|
||||||
|
});
|
||||||
|
this.productsControl.push(productFormGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeProduct(index: number) {
|
||||||
|
this.productsControl.removeAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
get products() {
|
||||||
|
return this.productsControl.controls as FormGroup[];
|
||||||
|
}
|
||||||
|
get canAddOrRemoveProducts() {
|
||||||
|
return this.isSingleProduct === false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<td>
|
||||||
|
{{ rowIndex + 1 }}
|
||||||
|
</td>
|
||||||
|
@if (editMode()) {
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
pInputText
|
||||||
|
type="text"
|
||||||
|
[value]="purchaseItemControl.controls.product.value?.sku"
|
||||||
|
class="w-full"
|
||||||
|
readonly
|
||||||
|
[attr.disabled]="true"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<products-select-field [control]="purchaseItemControl.controls.product" [showLabel]="false" [showErrors]="false" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<textarea
|
||||||
|
pTextarea
|
||||||
|
[formControl]="purchaseItemControl.controls.description"
|
||||||
|
rows="1"
|
||||||
|
[autoResize]="false"
|
||||||
|
></textarea>
|
||||||
|
</td>
|
||||||
|
<td><app-input [control]="purchaseItemControl.controls.fee" [showErrors]="false" /></td>
|
||||||
|
<td><app-input [control]="purchaseItemControl.controls.count" /></td>
|
||||||
|
<td><span [appPriceMask]="purchaseItemControl.controls.total.value || 0"></span></td>
|
||||||
|
<td>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button pButton type="button" icon="pi pi-check" text size="small" (click)="toggleEditMode()"></button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
} @else {
|
||||||
|
<td>{{ purchaseItemControl.controls.product.value?.sku || "" }}</td>
|
||||||
|
<td>{{ purchaseItemControl.controls.product.value?.name || "" }}</td>
|
||||||
|
<td>{{ purchaseItemControl.controls.description.value || "" }}</td>
|
||||||
|
<td><span [appPriceMask]="purchaseItemControl.controls.fee.value || 0"></span></td>
|
||||||
|
<td>{{ purchaseItemControl.controls.count.value }}</td>
|
||||||
|
<td><span [appPriceMask]="purchaseItemControl.controls.total.value || 0"></span></td>
|
||||||
|
<td>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button pButton type="button" icon="pi pi-pencil" text size="small" (click)="toggleEditMode()"></button>
|
||||||
|
@if (canRemove) {
|
||||||
|
<button pButton type="button" icon="pi pi-trash" text size="small" (click)="onDeleteClick()"></button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { ProductsSelectComponent } from '@/modules/products/components/select/select.component';
|
||||||
|
import { InputComponent } from '@/shared/components';
|
||||||
|
import { PriceMaskDirective } from '@/shared/directives';
|
||||||
|
import { Component, EventEmitter, Input, Output, signal } from '@angular/core';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { ButtonDirective } from 'primeng/button';
|
||||||
|
import { InputText } from 'primeng/inputtext';
|
||||||
|
import { Textarea } from 'primeng/textarea';
|
||||||
|
import { IPurchaseItemFormGroup } from '../models';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'tr[purchase-receipt-product-row]',
|
||||||
|
templateUrl: './purchase-receipt-product-row.component.html',
|
||||||
|
imports: [
|
||||||
|
ButtonDirective,
|
||||||
|
InputText,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
ProductsSelectComponent,
|
||||||
|
InputComponent,
|
||||||
|
Textarea,
|
||||||
|
PriceMaskDirective,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PurchaseReceiptProductRowComponent {
|
||||||
|
@Input() rowIndex!: number;
|
||||||
|
@Input() canRemove: boolean = true;
|
||||||
|
@Input() purchaseItemControl!: IPurchaseItemFormGroup;
|
||||||
|
|
||||||
|
@Output() onRemove = new EventEmitter<void>();
|
||||||
|
|
||||||
|
editMode = signal(false);
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.purchaseItemControl?.controls.count.valueChanges.subscribe(() => {
|
||||||
|
this.updateTotal();
|
||||||
|
});
|
||||||
|
this.purchaseItemControl?.controls.fee.valueChanges.subscribe(() => {
|
||||||
|
this.updateTotal();
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('purchase item control', this.purchaseItemControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTotal() {
|
||||||
|
this.purchaseItemControl.controls.total.setValue(
|
||||||
|
(this.purchaseItemControl.controls.count.value || 0) *
|
||||||
|
(this.purchaseItemControl.controls.fee.value || 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleEditMode() {
|
||||||
|
this.editMode.set(!this.editMode());
|
||||||
|
}
|
||||||
|
onDeleteClick() {
|
||||||
|
this.onRemove.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<form [formGroup]="form">
|
||||||
|
<p-card class="grid grid-cols-1 p-0 max-w-6xl mx-auto border border-surface-700">
|
||||||
|
<div class="mb-4 text-center">
|
||||||
|
<h2 class="m-0">رسید خرید</h2>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4 border-b border-surface-700 pb-4">
|
||||||
|
<div class="grid grid-cols-1 gap-2">
|
||||||
|
<shared-inline-edit>
|
||||||
|
<ng-template #data>
|
||||||
|
<app-key-value label="انبار" value="انبار انتخاب شده" />
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #field>
|
||||||
|
<inventories-select-field [control]="form.controls.inventoryId" />
|
||||||
|
</ng-template>
|
||||||
|
</shared-inline-edit>
|
||||||
|
<shared-inline-edit>
|
||||||
|
<ng-template #data>
|
||||||
|
<app-key-value label="تامین کننده" value="تامینکنندهی انتخاب شده" />
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #field>
|
||||||
|
<suppliers-select-field [control]="form.controls.supplierId" />
|
||||||
|
</ng-template>
|
||||||
|
</shared-inline-edit>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<div class="flex flex-col gap-2 max-w-xs items-end ms-auto w-full">
|
||||||
|
<shared-inline-edit class="w-full block">
|
||||||
|
<ng-template #data>
|
||||||
|
<app-key-value label="شماره رسید" value="۲۱۳۱۲۳" />
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #field>
|
||||||
|
<app-input label="شماره رسید" [control]="form.controls.code" />
|
||||||
|
</ng-template>
|
||||||
|
</shared-inline-edit>
|
||||||
|
<shared-inline-edit class="w-full block">
|
||||||
|
<ng-template #data>
|
||||||
|
<app-key-value label="تاریخ رسید" value="۲۱۳۱۲۳" />
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #field>
|
||||||
|
<app-input label="تاریخ خرید" [control]="form.controls.buyAt" />
|
||||||
|
</ng-template>
|
||||||
|
</shared-inline-edit>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p-table
|
||||||
|
[columns]="columns"
|
||||||
|
[value]="this.form.controls.products.controls"
|
||||||
|
[scrollable]="true"
|
||||||
|
[resizableColumns]="false"
|
||||||
|
[tableStyleClass]="'table-fixed'"
|
||||||
|
>
|
||||||
|
<ng-template #header let-columns>
|
||||||
|
<tr>
|
||||||
|
@for (col of columns; track $index) {
|
||||||
|
<th [style]="{ width: col.width, minWidth: col.minWidth }">
|
||||||
|
{{ col.header }}
|
||||||
|
</th>
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #body let-product let-columns="columns" let-rowIndex="rowIndex">
|
||||||
|
<tr
|
||||||
|
purchase-receipt-product-row
|
||||||
|
[rowIndex]="rowIndex"
|
||||||
|
[purchaseItemControl]="product"
|
||||||
|
[canRemove]="this.form.controls.products.controls.length > 1"
|
||||||
|
(onRemove)="removePurchaseItem(rowIndex)"
|
||||||
|
></tr>
|
||||||
|
<!-- <tr>
|
||||||
|
@for (col of columns; track $index) {
|
||||||
|
<td>
|
||||||
|
@if (col.field === "index") {
|
||||||
|
{{ rowIndex + 1 }}
|
||||||
|
} @else if (col.field === "name") {
|
||||||
|
<shared-inline-edit>
|
||||||
|
<ng-template #data>
|
||||||
|
{{ product.name || "-" }}
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #field>
|
||||||
|
<products-select-field
|
||||||
|
[control]="form.controls.products.controls[rowIndex].controls.productId"
|
||||||
|
[showLabel]="false"
|
||||||
|
></products-select-field>
|
||||||
|
</ng-template>
|
||||||
|
</shared-inline-edit>
|
||||||
|
} @else if (col.field === "total") {
|
||||||
|
{{ product["fee"] * product["count"] || 0 }}
|
||||||
|
} @else if (col.field === "action") {
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button></button>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
{{ product[col.field] }}
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
</tr> -->
|
||||||
|
</ng-template>
|
||||||
|
</p-table>
|
||||||
|
<div class="flex items-center justify-center py-3">
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
type="button"
|
||||||
|
icon="pi pi-plus"
|
||||||
|
size="large"
|
||||||
|
rounded
|
||||||
|
outlined
|
||||||
|
(click)="onAddProductClick()"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div class="flex border-t border-surface-700 items-stretch">
|
||||||
|
<div class="grow border-e border-surface-700"></div>
|
||||||
|
<div class="shrink-0 w-xs">
|
||||||
|
<div class="flex flex-col gap-2 p-4">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>جمع کل:</span>
|
||||||
|
<span [appPriceMask]="totalAmount"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>مالیات (۹٪):</span>
|
||||||
|
<span [appPriceMask]="totalAmount * 0.09"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex border-t border-surface-700 pt-4">
|
||||||
|
<div class="grow">
|
||||||
|
<span>مبلغ قابل پرداخت:</span>
|
||||||
|
<span [appPriceAlphabet]="totalAmount + totalAmount * 0.09"></span>
|
||||||
|
</div>
|
||||||
|
<div class="shrink-0 w-xs">
|
||||||
|
<span [appPriceMask]="totalAmount + totalAmount * 0.09"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center gap-2 mt-10">
|
||||||
|
<button pButton type="button" label="لغو" outlined (click)="onCancel()"></button>
|
||||||
|
<button pButton type="button" label="ثبت رسید" (click)="onSubmit()"></button>
|
||||||
|
</div>
|
||||||
|
</p-card>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import { Maybe } from '@/core';
|
||||||
|
import { InventoriesSelectComponent } from '@/modules/inventories/components/select/select.component';
|
||||||
|
import { IInventoryResponse } from '@/modules/inventories/models';
|
||||||
|
import { IProductResponse } from '@/modules/products/models';
|
||||||
|
import { SuppliersSelectComponent } from '@/modules/suppliers/components/select/select.component';
|
||||||
|
import { ISupplierResponse } from '@/modules/suppliers/models';
|
||||||
|
import { InlineEditComponent, InputComponent, KeyValueComponent } from '@/shared/components';
|
||||||
|
import { IColumn } from '@/shared/components/pageDataList/page-data-list.component';
|
||||||
|
import { PriceAlphabetDirective, PriceMaskDirective } from '@/shared/directives';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, inject, Input } from '@angular/core';
|
||||||
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { ButtonDirective } from 'primeng/button';
|
||||||
|
import { Card } from 'primeng/card';
|
||||||
|
import { TableModule } from 'primeng/table';
|
||||||
|
import { PurchaseReceiptProductRowComponent } from './purchase-receipt-product-row.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'purchase-receipt-template',
|
||||||
|
templateUrl: './purchase-receipt-template.component.html',
|
||||||
|
standalone: true,
|
||||||
|
|
||||||
|
imports: [
|
||||||
|
Card,
|
||||||
|
InventoriesSelectComponent,
|
||||||
|
SuppliersSelectComponent,
|
||||||
|
InputComponent,
|
||||||
|
TableModule,
|
||||||
|
InlineEditComponent,
|
||||||
|
KeyValueComponent,
|
||||||
|
PurchaseReceiptProductRowComponent,
|
||||||
|
PriceAlphabetDirective,
|
||||||
|
PriceMaskDirective,
|
||||||
|
ButtonDirective,
|
||||||
|
CommonModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PurchaseReceiptTemplateComponent {
|
||||||
|
private fb = inject(FormBuilder);
|
||||||
|
|
||||||
|
@Input() product?: IProductResponse;
|
||||||
|
@Input() inventory?: IInventoryResponse;
|
||||||
|
@Input() supplier?: ISupplierResponse;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.form.controls.products.valueChanges.subscribe((products) => {
|
||||||
|
let totalAmount = 0;
|
||||||
|
products.forEach((p: any) => {
|
||||||
|
totalAmount += p.total || 0;
|
||||||
|
});
|
||||||
|
this.form.controls.totalAmount.setValue(totalAmount);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
columns = [
|
||||||
|
{
|
||||||
|
field: 'index',
|
||||||
|
header: 'ردیف',
|
||||||
|
width: '60px',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sku',
|
||||||
|
header: 'کد کالا',
|
||||||
|
width: '120px',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
header: 'نام کالا',
|
||||||
|
minWidth: '160px',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
header: 'توضیحات',
|
||||||
|
width: '200px',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'fee',
|
||||||
|
header: 'قیمت واحد',
|
||||||
|
width: '120px',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'count',
|
||||||
|
header: 'تعداد',
|
||||||
|
width: '80px',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'total',
|
||||||
|
header: 'مبلغ کل',
|
||||||
|
width: '140px',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'action',
|
||||||
|
header: '',
|
||||||
|
width: '100px',
|
||||||
|
},
|
||||||
|
] as IColumn[];
|
||||||
|
|
||||||
|
form = this.fb.group({
|
||||||
|
totalAmount: [0, [Validators.required, Validators.min(0)]],
|
||||||
|
code: ['', [Validators.required]],
|
||||||
|
isSettled: [false],
|
||||||
|
buyAt: ['', Validators.required],
|
||||||
|
description: [''],
|
||||||
|
products: this.fb.array([
|
||||||
|
this.fb.group({
|
||||||
|
product: [null as Maybe<IProductResponse>, [Validators.required]],
|
||||||
|
count: [0, [Validators.required, Validators.min(1)]],
|
||||||
|
fee: [0, [Validators.required, Validators.min(0)]],
|
||||||
|
description: [''],
|
||||||
|
total: [0, [Validators.required, Validators.min(0)]],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
inventoryId: [this.inventory?.id || 0, [Validators.required]],
|
||||||
|
supplierId: [this.supplier?.id || 0, [Validators.required]],
|
||||||
|
});
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (this.product) {
|
||||||
|
this.form.controls.products.at(0).setValue({
|
||||||
|
product: this.product,
|
||||||
|
count: 0,
|
||||||
|
fee: 0,
|
||||||
|
description: '',
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalAmount() {
|
||||||
|
return this.form.controls.totalAmount.value || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddProductClick() {
|
||||||
|
this.form.controls.products.push(
|
||||||
|
this.fb.group({
|
||||||
|
product: [null as Maybe<IProductResponse>, [Validators.required]],
|
||||||
|
count: [0, [Validators.required, Validators.min(1)]],
|
||||||
|
fee: [0, [Validators.required, Validators.min(0)]],
|
||||||
|
description: [''],
|
||||||
|
total: [0, [Validators.required, Validators.min(0)]],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removePurchaseItem(index: number) {
|
||||||
|
this.form.controls.products.removeAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit() {
|
||||||
|
if (this.form.valid) {
|
||||||
|
// Handle form submission logic here
|
||||||
|
console.log('Form Submitted', this.form.value);
|
||||||
|
} else {
|
||||||
|
this.form.markAllAsTouched();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const BASE_URL = '/api/v1/purchase-receipts';
|
||||||
|
|
||||||
|
export const PURCHASES_API_ROUTES = {
|
||||||
|
getAll: BASE_URL,
|
||||||
|
getSingle: (id: number) => `${BASE_URL}/${id}`,
|
||||||
|
create: BASE_URL,
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './apiRoutes';
|
||||||
|
export * from './routes';
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
|
export type TProductChargesRouteNames = 'product-charges' | 'product-charge';
|
||||||
|
|
||||||
|
export const PRODUCT_CHARGES_ROUTES: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('../../views/list.component').then((m) => m.ProductChargesComponent),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':productChargeId',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('../../views/single.component').then((m) => m.ProductChargeComponent),
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Maybe } from '@/core';
|
||||||
|
import { IProductResponse } from '@/modules/products/models';
|
||||||
|
import { FormArray, FormControl, FormGroup } from '@angular/forms';
|
||||||
|
|
||||||
|
export * from './io';
|
||||||
|
|
||||||
|
export interface IPurchaseItemFormGroup extends FormGroup<{
|
||||||
|
product: FormControl<IProductResponse | null>;
|
||||||
|
count: FormControl<number>;
|
||||||
|
fee: FormControl<number>;
|
||||||
|
description: FormControl<string>;
|
||||||
|
total: FormControl<number>;
|
||||||
|
}> {}
|
||||||
|
|
||||||
|
export interface IPurchaseFormGroup extends FormGroup<{
|
||||||
|
code: FormControl<string>;
|
||||||
|
isSettled: FormControl<boolean>;
|
||||||
|
buyAt: FormControl<string>;
|
||||||
|
description: FormControl<Maybe<string>>;
|
||||||
|
products: FormArray<IPurchaseItemFormGroup>;
|
||||||
|
inventoryId: FormControl<number>;
|
||||||
|
supplierId: FormControl<number>;
|
||||||
|
}> {}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
import { Maybe } from '@/core';
|
||||||
|
|
||||||
|
export interface IPurchaseRawResponse {
|
||||||
|
id: number;
|
||||||
|
totalAmount: number;
|
||||||
|
inventoryId: number;
|
||||||
|
supplierId: number;
|
||||||
|
code: string;
|
||||||
|
items: IPurchaseItemResponse[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPurchaseResponse extends IPurchaseRawResponse {}
|
||||||
|
|
||||||
|
export interface IPurchaseItemRawResponse {
|
||||||
|
id: number;
|
||||||
|
count: number;
|
||||||
|
fee: number;
|
||||||
|
total: number;
|
||||||
|
productId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPurchaseRequest {
|
||||||
|
totalAmount: number;
|
||||||
|
inventoryId: number;
|
||||||
|
supplierId: number;
|
||||||
|
code?: string;
|
||||||
|
description?: string;
|
||||||
|
items: IPurchaseItemRequest[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPurchaseItemRequest {
|
||||||
|
productId: number;
|
||||||
|
count: number;
|
||||||
|
fee: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPurchaseForm {
|
||||||
|
inventory: Maybe<IInventoryResponse>;
|
||||||
|
supplier: Maybe<ISupplierResponse>;
|
||||||
|
code: string;
|
||||||
|
isSettled: boolean;
|
||||||
|
buyAt: Date | string;
|
||||||
|
description: string;
|
||||||
|
totalAmount: number;
|
||||||
|
products: IPurchaseItemForm[];
|
||||||
|
}
|
||||||
|
export interface IPurchaseItemForm {
|
||||||
|
product: Maybe<IProductResponse>;
|
||||||
|
count: number;
|
||||||
|
fee: number;
|
||||||
|
total: number;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class PurchasesService {
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
|
||||||
|
getAll(): Observable<{ data: IPurchaseRawResponse[] }> {
|
||||||
|
return this.http.get<{ data: IPurchaseResponse[] }>(PURCHASES_API_ROUTES.getAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSingle(id: number): Observable<{ data: IPurchaseResponse }> {
|
||||||
|
return this.http.get<{ data: IPurchaseResponse }>(PURCHASES_API_ROUTES.getSingle(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
create(data: IPurchaseRequest): Observable<{ data: IPurchaseRawResponse }> {
|
||||||
|
return this.http.post<{ data: IPurchaseResponse }>(PURCHASES_API_ROUTES.create, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { PageDataListComponent } from '@/shared/components/pageDataList/page-data-list.component';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-product-charges',
|
||||||
|
standalone: true,
|
||||||
|
imports: [PageDataListComponent],
|
||||||
|
templateUrl: './list.component.html',
|
||||||
|
})
|
||||||
|
export class ProductChargesComponent {}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<p>product-charge works!</p>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-product-charge',
|
||||||
|
standalone: true,
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './single.component.html',
|
||||||
|
})
|
||||||
|
export class ProductChargeComponent {}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<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.name" name="name" />
|
<app-input label="نام" [control]="form.controls.name" name="name" />
|
||||||
<app-input label="موقعیت" [control]="form.controls.location" name="location" />
|
<app-input label="موقعیت" [control]="form.controls.location" name="location" />
|
||||||
<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()" />
|
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
|
||||||
</form>
|
</form>
|
||||||
</p-dialog>
|
</p-dialog>
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ export class StoresComponent {
|
|||||||
{ field: 'name', header: 'نام' },
|
{ field: 'name', header: 'نام' },
|
||||||
{ field: 'location', header: 'موقعیت' },
|
{ field: 'location', header: 'موقعیت' },
|
||||||
{ field: 'isActive', header: 'فعال' },
|
{ field: 'isActive', header: 'فعال' },
|
||||||
{ field: 'createdAt', header: 'تاریخ ایجاد' },
|
{ field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' },
|
||||||
{ field: 'updatedAt', header: 'تاریخ بهروزرسانی' },
|
{ field: 'updatedAt', header: 'تاریخ بهروزرسانی', type: 'date' },
|
||||||
|
,
|
||||||
] as IColumn[];
|
] as IColumn[];
|
||||||
|
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<app-input label="شهر" [control]="form.controls.city" name="city" />
|
<app-input label="شهر" [control]="form.controls.city" name="city" />
|
||||||
<app-input label="استان" [control]="form.controls.state" name="state" />
|
<app-input label="استان" [control]="form.controls.state" name="state" />
|
||||||
<app-input label="کشور" [control]="form.controls.country" name="country" />
|
<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()" />
|
<app-form-footer-actions [submitLabel]="'ذخیره'" [loading]="form.disabled" (onCancel)="close()" />
|
||||||
</form>
|
</form>
|
||||||
</p-dialog>
|
</p-dialog>
|
||||||
|
|||||||
@@ -41,13 +41,13 @@ export class SupplierFormComponent {
|
|||||||
form = this.fb.group({
|
form = this.fb.group({
|
||||||
firstName: [this.initialValues?.firstName || null, [Validators.required]],
|
firstName: [this.initialValues?.firstName || null, [Validators.required]],
|
||||||
lastName: [this.initialValues?.lastName || 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]],
|
mobileNumber: [this.initialValues?.mobileNumber || null, [Validators.required]],
|
||||||
address: [this.initialValues?.address || null, [Validators.required]],
|
email: [this.initialValues?.email || null, [Validators.email]],
|
||||||
city: [this.initialValues?.city || null, [Validators.required]],
|
address: [this.initialValues?.address || null],
|
||||||
state: [this.initialValues?.state || null, [Validators.required]],
|
city: [this.initialValues?.city || null],
|
||||||
country: [this.initialValues?.country || null, [Validators.required]],
|
state: [this.initialValues?.state || null],
|
||||||
isActive: [this.initialValues?.isActive || false, [Validators.required]],
|
country: [this.initialValues?.country || null],
|
||||||
|
isActive: [this.initialValues?.isActive || false],
|
||||||
});
|
});
|
||||||
|
|
||||||
submitLoading = signal(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
@@ -14,7 +14,9 @@ export interface ISupplierRawResponse {
|
|||||||
deletedAt: Date;
|
deletedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISupplierResponse extends ISupplierRawResponse {}
|
export interface ISupplierResponse extends ISupplierRawResponse {
|
||||||
|
fullname: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ISupplierRequest {
|
export interface ISupplierRequest {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { IPaginatedResponse } from '@/core/models/service.model';
|
import { IPaginatedResponse } from '@/core/models/service.model';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { map, Observable } from 'rxjs';
|
||||||
import { SUPPLIERS_API_ROUTES } from '../constants';
|
import { SUPPLIERS_API_ROUTES } from '../constants';
|
||||||
import { ISupplierRawResponse, ISupplierRequest, ISupplierResponse } from '../models';
|
import { ISupplierRawResponse, ISupplierRequest, ISupplierResponse } from '../models';
|
||||||
|
|
||||||
@@ -12,14 +12,32 @@ export class SuppliersService {
|
|||||||
private apiRoutes = SUPPLIERS_API_ROUTES;
|
private apiRoutes = SUPPLIERS_API_ROUTES;
|
||||||
|
|
||||||
getAll(): Observable<IPaginatedResponse<ISupplierResponse>> {
|
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> {
|
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> {
|
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 = [
|
columns = [
|
||||||
{ field: 'id', header: 'شناسه' },
|
{
|
||||||
{ field: 'firstName', header: 'نام' },
|
field: 'name',
|
||||||
{ field: 'lastName', header: 'نام خانوادگی' },
|
header: 'نام',
|
||||||
{ field: 'email', header: 'ایمیل' },
|
customDataModel(item) {
|
||||||
|
return `${item.firstName} ${item.lastName}` || '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
{ field: 'mobileNumber', header: 'شماره موبایل' },
|
{ field: 'mobileNumber', header: 'شماره موبایل' },
|
||||||
{ field: 'address', header: 'آدرس' },
|
{ field: 'address', header: 'آدرس' },
|
||||||
{ field: 'city', header: 'شهر' },
|
{
|
||||||
{ field: 'state', header: 'استان' },
|
field: 'isActive',
|
||||||
{ field: 'country', header: 'کشور' },
|
header: 'وضعیت',
|
||||||
{ field: 'isActive', header: 'فعال' },
|
customDataModel(item) {
|
||||||
{ field: 'createdAt', header: 'تاریخ ایجاد' },
|
return item.isActive ? 'فعال' : 'غیرفعال';
|
||||||
{ field: 'updatedAt', header: 'تاریخ بهروزرسانی' },
|
},
|
||||||
|
},
|
||||||
|
{ field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' },
|
||||||
|
{ field: 'updatedAt', header: 'تاریخ بهروزرسانی', type: 'date' },
|
||||||
] as IColumn[];
|
] as IColumn[];
|
||||||
|
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from './breadcrumb.component';
|
export * from './breadcrumb.component';
|
||||||
export * from './card-data.component';
|
export * from './card-data.component';
|
||||||
export * from './inlineConfirmation/inline-confirmation.component';
|
export * from './inlineConfirmation/inline-confirmation.component';
|
||||||
|
export * from './inlineEdit/inline-edit.component';
|
||||||
export * from './input/input.component';
|
export * from './input/input.component';
|
||||||
export * from './key-value.component/key-value.component';
|
export * from './key-value.component/key-value.component';
|
||||||
export * from './table-action-row.component';
|
export * from './table-action-row.component';
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<div class="flex items-stretch gap-2">
|
||||||
|
<div class="shrink-0 flex items-center">
|
||||||
|
@if (editMode()) {
|
||||||
|
<ng-container [ngTemplateOutlet]="field"></ng-container>
|
||||||
|
} @else {
|
||||||
|
<ng-container [ngTemplateOutlet]="data"></ng-container>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="relative shrink-0">
|
||||||
|
<div class="static end-0 top-0">
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
[icon]="'pi ' + (editMode() ? 'pi-times' : 'pi-pencil')"
|
||||||
|
text
|
||||||
|
severity="secondary"
|
||||||
|
(click)="toggleEditMode()"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, ContentChild, signal, TemplateRef } from '@angular/core';
|
||||||
|
import { ButtonDirective } from 'primeng/button';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'shared-inline-edit',
|
||||||
|
templateUrl: './inline-edit.component.html',
|
||||||
|
imports: [CommonModule, ButtonDirective],
|
||||||
|
})
|
||||||
|
export class InlineEditComponent {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
editMode = signal<boolean>(false);
|
||||||
|
@ContentChild('data', { static: true }) data?: TemplateRef<any>;
|
||||||
|
@ContentChild('field', { static: true }) field?: TemplateRef<any>;
|
||||||
|
|
||||||
|
toggleEditMode() {
|
||||||
|
this.editMode.set(!this.editMode());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<uikit-field [label]="label" [name]="name" [control]="control">
|
<uikit-field [label]="label" [name]="name" [control]="control" [showLabel]="!!label" [showErrors]="showErrors">
|
||||||
@if (type === "switch") {
|
@if (type === "switch") {
|
||||||
<p-toggleSwitch [formControl]="control" />
|
<p-toggleSwitch [formControl]="control" />
|
||||||
} @else {
|
} @else {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export class InputComponent {
|
|||||||
@Input() disabled = false;
|
@Input() disabled = false;
|
||||||
@Input() size?: 'small' | 'large';
|
@Input() size?: 'small' | 'large';
|
||||||
@Input() autocomplete?: string = 'off';
|
@Input() autocomplete?: string = 'off';
|
||||||
|
@Input() showErrors = false;
|
||||||
@Output() valueChange = new EventEmitter<string>();
|
@Output() valueChange = new EventEmitter<string>();
|
||||||
|
|
||||||
onInput(ev: Event) {
|
onInput(ev: Event) {
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ import { SkeletonModule } from 'primeng/skeleton';
|
|||||||
import { TableModule } from 'primeng/table';
|
import { TableModule } from 'primeng/table';
|
||||||
import { TableActionRowComponent } from '../table-action-row.component';
|
import { TableActionRowComponent } from '../table-action-row.component';
|
||||||
|
|
||||||
export interface IColumn {
|
export interface IColumn<T = any> {
|
||||||
field: string;
|
field: T extends object ? keyof T | string : string;
|
||||||
header: string;
|
header: string;
|
||||||
width?: string;
|
width?: string;
|
||||||
minWidth?: string;
|
minWidth?: string;
|
||||||
canCopy?: boolean;
|
canCopy?: boolean;
|
||||||
type?: 'text' | 'price' | 'boolean' | 'date';
|
type?: 'text' | 'price' | 'boolean' | 'date';
|
||||||
customDataModel?: TemplateRef<any> | ((item: any) => string | number | boolean);
|
customDataModel?: TemplateRef<any> | ((item: T) => string | number | boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -120,7 +120,7 @@ export class PageDataListComponent<I> {
|
|||||||
if (column.customDataModel) {
|
if (column.customDataModel) {
|
||||||
return this.renderCustom(column, item);
|
return this.renderCustom(column, item);
|
||||||
}
|
}
|
||||||
const data = item[field];
|
const data = item[String(field)];
|
||||||
switch (column.type) {
|
switch (column.type) {
|
||||||
case 'date':
|
case 'date':
|
||||||
if (!data) return '-';
|
if (!data) return '-';
|
||||||
|
|||||||
@@ -20,6 +20,6 @@
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@if (showDetails) {
|
@if (showDetails) {
|
||||||
<p-button size="small" icon="pi pi-chevron-left" variant="outlined" (click)="details.emit()" title="جزئیات" />
|
<p-button size="small" icon="pi pi-chevron-left" (click)="details.emit()" title="جزئیات" />
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './price-alphabet.directive';
|
||||||
|
export * from './price-mask.directive';
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { getPriceInPersianAlphabet } from '@/utils';
|
||||||
|
import { Directive, ElementRef, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[appPriceAlphabet]',
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class PriceAlphabetDirective implements OnChanges {
|
||||||
|
/** Numeric price to render as Persian text */
|
||||||
|
@Input('appPriceAlphabet') price: number | string | null = null;
|
||||||
|
|
||||||
|
constructor(private el: ElementRef<HTMLElement>) {}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (changes['price']) {
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private render() {
|
||||||
|
const value = this.normalizeToNumber(this.price);
|
||||||
|
if (value === null) {
|
||||||
|
this.el.nativeElement.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.el.nativeElement.textContent = getPriceInPersianAlphabet(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize various input shapes to a number; returns null when not parsable
|
||||||
|
*/
|
||||||
|
private normalizeToNumber(raw: number | string | null): number | null {
|
||||||
|
if (raw === null || raw === undefined) return null;
|
||||||
|
if (typeof raw === 'number') {
|
||||||
|
return isFinite(raw) ? raw : null;
|
||||||
|
}
|
||||||
|
// remove common separators and whitespace before parsing
|
||||||
|
const cleaned = raw.replace(/[,\s]/g, '');
|
||||||
|
const num = Number(cleaned);
|
||||||
|
return isNaN(num) ? null : num;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,12 @@ import {
|
|||||||
ElementRef,
|
ElementRef,
|
||||||
HostListener,
|
HostListener,
|
||||||
Input,
|
Input,
|
||||||
|
OnChanges,
|
||||||
OnInit,
|
OnInit,
|
||||||
Optional,
|
Optional,
|
||||||
Renderer2,
|
Renderer2,
|
||||||
Self,
|
Self,
|
||||||
|
SimpleChanges,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { NgControl } from '@angular/forms';
|
import { NgControl } from '@angular/forms';
|
||||||
|
|
||||||
@@ -14,7 +16,9 @@ import { NgControl } from '@angular/forms';
|
|||||||
selector: '[appPriceMask]',
|
selector: '[appPriceMask]',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
export class PriceMaskDirective implements OnInit {
|
export class PriceMaskDirective implements OnInit, OnChanges {
|
||||||
|
/** Optional value to render/format when bound like [appPriceMask]="price" */
|
||||||
|
@Input('appPriceMask') priceValue: number | string | null | undefined;
|
||||||
@Input('appPriceMaskLocale') locale: string = 'fa-IR';
|
@Input('appPriceMaskLocale') locale: string = 'fa-IR';
|
||||||
@Input('appPriceMaskFraction') fraction = 0;
|
@Input('appPriceMaskFraction') fraction = 0;
|
||||||
/**
|
/**
|
||||||
@@ -22,6 +26,8 @@ export class PriceMaskDirective implements OnInit {
|
|||||||
* If false, uses the provided `appPriceMaskLocale`.
|
* If false, uses the provided `appPriceMaskLocale`.
|
||||||
*/
|
*/
|
||||||
@Input('appPriceMaskUseComma') useComma = false;
|
@Input('appPriceMaskUseComma') useComma = false;
|
||||||
|
/** Optional currency code to append for non-input hosts (e.g., "ریال" | "تومان") */
|
||||||
|
@Input('appPriceMaskCurrency') currency: string | null = 'ریال';
|
||||||
|
|
||||||
private inputEl!: HTMLInputElement | null;
|
private inputEl!: HTMLInputElement | null;
|
||||||
|
|
||||||
@@ -38,6 +44,17 @@ export class PriceMaskDirective implements OnInit {
|
|||||||
host.tagName.toLowerCase() === 'input'
|
host.tagName.toLowerCase() === 'input'
|
||||||
? (host as HTMLInputElement)
|
? (host as HTMLInputElement)
|
||||||
: host.querySelector('input');
|
: host.querySelector('input');
|
||||||
|
|
||||||
|
// If a value is bound initially (e.g., span with [appPriceMask]), render it
|
||||||
|
if (this.priceValue !== undefined) {
|
||||||
|
this.renderFromBoundValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (changes['priceValue'] && this.priceValue !== undefined) {
|
||||||
|
this.renderFromBoundValue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private toArabicDigitsAwareNumber(str: string): string {
|
private toArabicDigitsAwareNumber(str: string): string {
|
||||||
@@ -74,6 +91,15 @@ export class PriceMaskDirective implements OnInit {
|
|||||||
return ascii.replace(/[^0-9.\-]/g, '');
|
return ascii.replace(/[^0-9.\-]/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeToNumber(raw: number | string | null | undefined): number | null {
|
||||||
|
if (raw === null || raw === undefined) return null;
|
||||||
|
if (typeof raw === 'number') return isFinite(raw) ? raw : null;
|
||||||
|
const cleaned = this.cleanNumericString(String(raw));
|
||||||
|
if (cleaned === '') return null;
|
||||||
|
const num = Number(cleaned);
|
||||||
|
return isNaN(num) ? null : num;
|
||||||
|
}
|
||||||
|
|
||||||
private formatNumber(value: number): string {
|
private formatNumber(value: number): string {
|
||||||
try {
|
try {
|
||||||
const fmtLocale = this.useComma ? 'en-US' : this.locale;
|
const fmtLocale = this.useComma ? 'en-US' : this.locale;
|
||||||
@@ -87,6 +113,17 @@ export class PriceMaskDirective implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private formatWithCurrency(num: number | null, isInputHost: boolean): string {
|
||||||
|
if (num === null) return '';
|
||||||
|
const formatted = this.formatNumber(num);
|
||||||
|
// For inputs we should NOT append currency to avoid polluting numeric entry
|
||||||
|
if (isInputHost) return formatted;
|
||||||
|
if (this.currency && this.currency.trim()) {
|
||||||
|
return `${formatted} ${this.currency.trim()}`;
|
||||||
|
}
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
@HostListener('input', ['$event'])
|
@HostListener('input', ['$event'])
|
||||||
onInput(ev: Event) {
|
onInput(ev: Event) {
|
||||||
if (!this.inputEl) return;
|
if (!this.inputEl) return;
|
||||||
@@ -128,4 +165,28 @@ export class PriceMaskDirective implements OnInit {
|
|||||||
// ignore selection errors
|
// ignore selection errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Render when bound via [appPriceMask] on non-input hosts (e.g., span) or to set initial value. */
|
||||||
|
private renderFromBoundValue() {
|
||||||
|
const num = this.normalizeToNumber(this.priceValue);
|
||||||
|
|
||||||
|
// If host has an input element, set both control value (if any) and displayed value
|
||||||
|
if (this.inputEl) {
|
||||||
|
if (this.ngControl?.control) {
|
||||||
|
try {
|
||||||
|
this.ngControl.control.setValue(num, { emitEvent: false });
|
||||||
|
} catch (err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = this.formatWithCurrency(num, true);
|
||||||
|
this.renderer.setProperty(this.inputEl, 'value', formatted);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, render to host text (e.g., span/div)
|
||||||
|
const formatted = this.formatWithCurrency(num, false);
|
||||||
|
this.renderer.setProperty(this.el.nativeElement, 'textContent', formatted);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,25 @@
|
|||||||
<div class="flex flex-col gap-1">
|
<ng-container [ngTemplateOutlet]="fieldTemplate" />
|
||||||
<uikit-label [name]="name" [invalid]="!!invalid || showErrors">
|
|
||||||
{{ label }}
|
<ng-template #fieldTemplate>
|
||||||
</uikit-label>
|
@if (showLabel) {
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<uikit-label [name]="name" [invalid]="!!invalid || hasError">
|
||||||
|
{{ label }}
|
||||||
|
</uikit-label>
|
||||||
|
<ng-container [ngTemplateOutlet]="contentTemplate" />
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<ng-container [ngTemplateOutlet]="contentTemplate" />
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #contentTemplate>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
@if (showErrors) {
|
@if (showErrors && hasError) {
|
||||||
<ng-container>
|
<ng-container>
|
||||||
<p-message severity="error" size="small" variant="simple" class="formHint">
|
<p-message severity="error" size="small" variant="simple" class="formHint">
|
||||||
{{ errors[0] }}
|
{{ errors[0] }}
|
||||||
</p-message>
|
</p-message>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
}
|
}
|
||||||
</div>
|
</ng-template>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type PSize = 'small' | 'normal' | 'large';
|
|||||||
templateUrl: './uikit-field.component.html',
|
templateUrl: './uikit-field.component.html',
|
||||||
})
|
})
|
||||||
export class UikitFieldComponent {
|
export class UikitFieldComponent {
|
||||||
@Input() label!: string;
|
@Input() label: string = '';
|
||||||
@Input() name!: string;
|
@Input() name!: string;
|
||||||
@Input() pSize: PSize = 'normal';
|
@Input() pSize: PSize = 'normal';
|
||||||
@Input() className?: string;
|
@Input() className?: string;
|
||||||
@@ -22,9 +22,12 @@ export class UikitFieldComponent {
|
|||||||
// accept a FormControl or AbstractControl to display errors for
|
// accept a FormControl or AbstractControl to display errors for
|
||||||
@Input() control?: AbstractControl | null;
|
@Input() control?: AbstractControl | null;
|
||||||
|
|
||||||
|
@Input() showErrors: boolean = true;
|
||||||
|
@Input() showLabel: boolean = true;
|
||||||
|
|
||||||
private errorsService = inject(FormErrorsService);
|
private errorsService = inject(FormErrorsService);
|
||||||
|
|
||||||
get showErrors(): boolean {
|
get hasError(): boolean {
|
||||||
if (!this.control) return !!this.invalid;
|
if (!this.control) return !!this.invalid;
|
||||||
return !!(this.control.invalid && (this.control.touched || this.control.dirty));
|
return !!(this.control.invalid && (this.control.touched || this.control.dirty));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Persian number words for currency conversion
|
||||||
|
*/
|
||||||
|
const ONES = ['', 'یک', 'دو', 'سه', 'چهار', 'پنج', 'شش', 'هفت', 'هشت', 'نه'];
|
||||||
|
|
||||||
|
const TENS = ['', '', 'بیست', 'سی', 'چهل', 'پنجاه', 'شصت', 'هفتاد', 'هشتاد', 'نود'];
|
||||||
|
|
||||||
|
const HUNDREDS = ['', 'یکصد', 'دویست', 'سیصد', 'چهارصد', 'پانصد', 'ششصد', 'هفتصد', 'هشتصد', 'نهصد'];
|
||||||
|
|
||||||
|
const SCALE = ['', 'هزار', 'میلیون', 'میلیارد', 'تریلیون', 'کوادریلیون'];
|
||||||
|
|
||||||
|
const TEENS = [
|
||||||
|
'ده',
|
||||||
|
'یازده',
|
||||||
|
'دوازده',
|
||||||
|
'سیزده',
|
||||||
|
'چهارده',
|
||||||
|
'پانزده',
|
||||||
|
'شانزده',
|
||||||
|
'هفده',
|
||||||
|
'هجده',
|
||||||
|
'نوزده',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a three-digit number to Persian words
|
||||||
|
*/
|
||||||
|
function convertThreeDigits(num: number): string {
|
||||||
|
if (num === 0) return '';
|
||||||
|
|
||||||
|
const hundred = Math.floor(num / 100);
|
||||||
|
const remainder = num % 100;
|
||||||
|
const ten = Math.floor(remainder / 10);
|
||||||
|
const one = remainder % 10;
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
if (hundred > 0) {
|
||||||
|
result += HUNDREDS[hundred];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainder >= 10 && remainder <= 19) {
|
||||||
|
if (result) result += ' و ';
|
||||||
|
result += TEENS[remainder - 10];
|
||||||
|
} else {
|
||||||
|
if (ten > 0) {
|
||||||
|
if (result) result += ' و ';
|
||||||
|
result += TENS[ten];
|
||||||
|
}
|
||||||
|
if (one > 0) {
|
||||||
|
if (result) result += ' و ';
|
||||||
|
result += ONES[one];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a number to Persian words for Rial currency
|
||||||
|
* @param price - The price as a number
|
||||||
|
* @returns Persian text representation of the price in Rials
|
||||||
|
* @example
|
||||||
|
* getPriceInPersianAlphabet(1500) // "هزار و پانصد ریال"
|
||||||
|
* getPriceInPersianAlphabet(1000000) // "یک میلیون ریال"
|
||||||
|
*/
|
||||||
|
export function getPriceInPersianAlphabet(price: number): string {
|
||||||
|
if (price === 0) return 'صفر ریال';
|
||||||
|
if (price < 0) return 'منفی ' + getPriceInPersianAlphabet(-price);
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
let scaleIndex = 0;
|
||||||
|
|
||||||
|
while (price > 0 && scaleIndex < SCALE.length) {
|
||||||
|
const threeDigits = price % 1000;
|
||||||
|
|
||||||
|
if (threeDigits > 0) {
|
||||||
|
const threeDigitWords = convertThreeDigits(threeDigits);
|
||||||
|
if (SCALE[scaleIndex]) {
|
||||||
|
parts.unshift(threeDigitWords + ' ' + SCALE[scaleIndex]);
|
||||||
|
} else {
|
||||||
|
parts.unshift(threeDigitWords);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
price = Math.floor(price / 1000);
|
||||||
|
scaleIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' و ') + ' ریال';
|
||||||
|
}
|
||||||
@@ -1 +1,3 @@
|
|||||||
|
export * from './currency';
|
||||||
|
export * from './name';
|
||||||
export * from './time';
|
export * from './time';
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
export interface NameParts {
|
||||||
|
gender?: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full name from name parts
|
||||||
|
* @param nameParts - Object containing optional gender, firstName, and lastName
|
||||||
|
* @returns Full name string with parts separated by space
|
||||||
|
*/
|
||||||
|
export function getFullName(nameParts: NameParts): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (nameParts.gender) {
|
||||||
|
parts.push(nameParts.gender);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nameParts.firstName) {
|
||||||
|
parts.push(nameParts.firstName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nameParts.lastName) {
|
||||||
|
parts.push(nameParts.lastName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' ').trim();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user