feat(purchases): update terminology from "محصول" to "کالا" in purchase forms and templates

refactor(purchases): remove unused purchase receipt components and consolidate receipt template logic

fix(suppliers): enhance suppliers select component to support full data option values

style(tailwind): improve theme variables for better customization and consistency

feat(shared): introduce abstract select component for reusable select field logic

chore(purchases): clean up imports and improve component structure for better maintainability
This commit is contained in:
2025-12-10 16:54:25 +03:30
parent 50bc9a4632
commit b46b8b83f9
43 changed files with 450 additions and 391 deletions
+4 -4
View File
@@ -21,21 +21,21 @@ export const MENU_ITEMS = [
],
},
{
label: 'محصولات',
label: 'کالاها',
icon: 'pi pi-fw pi-tags',
items: [
{
label: 'لیست محصولات',
label: 'لیست کالاها',
icon: 'pi pi-fw pi-list',
routerLink: ['/products'],
},
{
label: 'برندهای محصول',
label: 'برندهای کالا',
icon: 'pi pi-fw pi-tags',
routerLink: ['/product-brands'],
},
{
label: 'دسته‌بندی‌های محصول',
label: 'دسته‌بندی‌های کالا',
icon: 'pi pi-fw pi-list',
routerLink: ['/product-categories'],
},
@@ -1,9 +1,8 @@
<div class="">
<uikit-field label="مشتری">
<uikit-field label="مشتری" [control]="control" [showLabel]="showLabel" [showErrors]="showErrors">
<p-select
[options]="brands()"
[options]="items()"
optionLabel="name"
optionValue="id"
[optionValue]="selectOptionValue"
placeholder="انتخاب مشتری"
[formControl]="control"
[showClear]="true"
@@ -12,4 +11,3 @@
>
</p-select>
</uikit-field>
</div>
@@ -1,7 +1,7 @@
import { Maybe } from '@/core';
import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit';
import { Component, Input, signal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { Component } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Select } from 'primeng/select';
import { ICustomerResponse } from '../../models';
import { CustomersService } from '../../services/main.service';
@@ -11,24 +11,17 @@ import { CustomersService } from '../../services/main.service';
templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent],
})
export class CustomersSelectComponent {
@Input() control!: FormControl<Maybe<number>>;
@Input() canInsert: boolean = false;
export class CustomersSelectComponent extends AbstractSelectComponent<ICustomerResponse> {
constructor(private service: CustomersService) {
super();
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.items.set(res.data);
this.loading.set(false);
},
error: () => {
@@ -36,12 +29,4 @@ export class CustomersSelectComponent {
},
});
}
refresh() {
this.getData();
}
onOpenForm() {
// this.isOpenFormDialog.set(true);
}
}
@@ -1,9 +1,8 @@
<div class="">
<uikit-field label="انبار">
<uikit-field label="انبار" [control]="control" [showLabel]="showLabel" [showErrors]="showErrors">
<p-select
[options]="brands()"
[options]="items()"
optionLabel="name"
optionValue="id"
[optionValue]="selectOptionValue"
placeholder="انتخاب انبار"
[formControl]="control"
[showClear]="true"
@@ -12,4 +11,3 @@
>
</p-select>
</uikit-field>
</div>
@@ -1,7 +1,7 @@
import { Maybe } from '@/core';
import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit';
import { Component, Input, signal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { Component } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Select } from 'primeng/select';
import { IInventoryResponse } from '../../models';
import { InventoriesService } from '../../services/main.service';
@@ -11,24 +11,17 @@ import { InventoriesService } from '../../services/main.service';
templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent],
})
export class InventoriesSelectComponent {
@Input() control!: FormControl<Maybe<number>>;
@Input() canInsert: boolean = false;
export class InventoriesSelectComponent extends AbstractSelectComponent<IInventoryResponse> {
constructor(private service: InventoriesService) {
super();
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.items.set(res.data);
this.loading.set(false);
},
error: () => {
@@ -36,12 +29,4 @@ export class InventoriesSelectComponent {
},
});
}
refresh() {
this.getData();
}
onOpenForm() {
// this.isOpenFormDialog.set(true);
}
}
@@ -1,5 +1,5 @@
<p-dialog
header="فرم برند محصول"
header="فرم برند کالا"
[(visible)]="visible"
[modal]="true"
[style]="{ width: '500px' }"
@@ -1,9 +1,9 @@
<div class="">
<uikit-field label="برند">
<div class="w-full">
<uikit-field label="برند" [control]="control" [showLabel]="showLabel" [showErrors]="showErrors">
<p-select
[options]="brands()"
[options]="items()"
optionLabel="name"
optionValue="id"
[optionValue]="selectOptionValue"
placeholder="انتخاب برند"
[formControl]="control"
[showClear]="true"
@@ -1,7 +1,7 @@
import { Maybe } from '@/core';
import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit';
import { Component, Input, signal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { Component } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Button } from 'primeng/button';
import { Select } from 'primeng/select';
import { IProductBrandResponse } from '../../models';
@@ -13,24 +13,17 @@ import { ProductBrandFormComponent } from '../form.component';
templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent, Button, ProductBrandFormComponent],
})
export class ProductBrandsSelectComponent {
@Input() control!: FormControl<Maybe<number>>;
@Input() canInsert: boolean = false;
export class ProductBrandsSelectComponent extends AbstractSelectComponent<IProductBrandResponse> {
constructor(private service: ProductBrandsService) {
super();
this.getData();
}
loading = signal(false);
brands = signal<Maybe<IProductBrandResponse[]>>(null);
isOpenFormDialog = signal(false);
getData() {
this.loading.set(true);
this.service.getAll().subscribe({
next: (res) => {
this.brands.set(res.data);
this.items.set(res.data);
this.loading.set(false);
},
error: () => {
@@ -38,12 +31,4 @@ export class ProductBrandsSelectComponent {
},
});
}
refresh() {
this.getData();
}
onOpenForm() {
this.isOpenFormDialog.set(true);
}
}
@@ -8,7 +8,7 @@ export const productBrandsNamedRoutes: NamedRoutes<TProductBrandsRouteNames> = {
path: 'product-brands',
loadComponent: () => import('../../views/list.component').then((m) => m.ProductBrandsComponent),
meta: {
title: 'برندهای محصول',
title: 'برندهای کالا',
pagePath: () => '/product-brands',
},
},
@@ -17,7 +17,7 @@ export const productBrandsNamedRoutes: NamedRoutes<TProductBrandsRouteNames> = {
loadComponent: () =>
import('../../views/single.component').then((m) => m.ProductBrandComponent),
meta: {
title: 'برند محصول',
title: 'برند کالا',
pagePath: () => '/product-brands/:brandId',
},
},
@@ -1,5 +1,5 @@
<app-page-data-list
[pageTitle]="'مدیریت برندهای محصول'"
[pageTitle]="'مدیریت برندهای کالا'"
[addNewCtaLabel]="'افزودن برند جدید'"
[columns]="columns"
[items]="items()"
@@ -1,5 +1,5 @@
<p-dialog
header="فرم دسته‌بندی محصول"
header="فرم دسته‌بندی کالا"
[(visible)]="visible"
[modal]="true"
[style]="{ width: '500px' }"
@@ -1,9 +1,9 @@
<div class="">
<uikit-field label="دسته‌بندی">
<div class="w-full">
<uikit-field label="دسته‌بندی" [control]="control" [showLabel]="showLabel" [showErrors]="showErrors">
<p-select
[options]="brands()"
[options]="items()"
optionLabel="name"
optionValue="id"
[optionValue]="selectOptionValue"
placeholder="انتخاب دسته‌بندی"
[formControl]="control"
[showClear]="true"
@@ -1,7 +1,7 @@
import { Maybe } from '@/core';
import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit';
import { Component, Input, signal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { Component } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Button } from 'primeng/button';
import { Select } from 'primeng/select';
import { IProductCategoryResponse } from '../../models';
@@ -13,24 +13,17 @@ import { ProductCategoryFormComponent } from '../form.component';
templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent, Button, ProductCategoryFormComponent],
})
export class ProductCategoriesSelectComponent {
@Input() control!: FormControl<Maybe<number>>;
@Input() canInsert: boolean = false;
export class ProductCategoriesSelectComponent extends AbstractSelectComponent<IProductCategoryResponse> {
constructor(private service: ProductCategoriesService) {
super();
this.getData();
}
loading = signal(false);
brands = signal<Maybe<IProductCategoryResponse[]>>(null);
isOpenFormDialog = signal(false);
getData() {
this.loading.set(true);
this.service.getAll().subscribe({
next: (res) => {
this.brands.set(res.data);
this.items.set(res.data);
this.loading.set(false);
},
error: () => {
@@ -38,12 +31,4 @@ export class ProductCategoriesSelectComponent {
},
});
}
refresh() {
this.getData();
}
onOpenForm() {
this.isOpenFormDialog.set(true);
}
}
@@ -9,7 +9,7 @@ export const productCategoriesNamedRoutes: NamedRoutes<TProductCategoriesRouteNa
loadComponent: () =>
import('../../views/list.component').then((m) => m.ProductCategoriesComponent),
meta: {
title: 'دسته‌بندی محصولات',
title: 'دسته‌بندی کالاها',
pagePath: () => '/product-categories',
},
},
@@ -18,7 +18,7 @@ export const productCategoriesNamedRoutes: NamedRoutes<TProductCategoriesRouteNa
loadComponent: () =>
import('../../views/single.component').then((m) => m.ProductCategoryComponent),
meta: {
title: 'دسته‌بندی محصول',
title: 'دسته‌بندی کالا',
pagePath: () => '/product-categories/:categoryId',
},
},
@@ -1,5 +1,5 @@
<app-page-data-list
[pageTitle]="'مدیریت دسته‌بندی محصولات'"
[pageTitle]="'مدیریت دسته‌بندی کالاها'"
[addNewCtaLabel]="'افزودن دسته‌بندی جدید'"
[columns]="columns"
[showAdd]="true"
@@ -1,4 +1,4 @@
<p-dialog [(visible)]="visible" [modal]="true" [style]="{ width: '50vw' }" header="افزودن هزینه محصول">
<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" />
@@ -28,7 +28,7 @@ export class ProductChargesComponent {
{ field: 'totalAmount', header: 'مبلغ کل' },
{ field: 'isSettled', header: 'تسویه شده' },
{ field: 'buyAt', header: 'تاریخ خرید' },
{ field: 'productId', header: 'شناسه محصول' },
{ field: 'productId', header: 'شناسه کالا' },
{ field: 'inventoryId', header: 'شناسه انبار' },
{ field: 'supplierId', header: 'شناسه تامین‌کننده' },
{ field: 'createdAt', header: 'تاریخ ایجاد' },
@@ -1,10 +1,10 @@
<p-dialog header="فرم محصول" [(visible)]="visible" [modal]="true" [style]="{ width: '600px' }" [closable]="true">
<p-dialog header="فرم کالا" [(visible)]="visible" [modal]="true" [style]="{ width: '600px' }" [closable]="true">
<form [formGroup]="form" (submit)="submit()" class="flex flex-col gap-4">
<app-input label="نام" [control]="form.controls.name" name="name" />
<product-brands-select-field [control]="form.controls.brandId" [canInsert]="true" />
<product-categories-select-field [control]="form.controls.categoryId" [canInsert]="true" />
<app-input label="توضیحات" [control]="form.controls.description" name="description" />
<!-- <app-input label="نوع محصول" [control]="form.controls.productType" name="productType" /> -->
<!-- <app-input label="نوع کالا" [control]="form.controls.productType" name="productType" /> -->
<!-- <app-input label="برند" [control]="form.controls.brandId" name="brandId" type="" />
<app-input label="دسته‌بندی" [control]="form.controls.categoryId" name="categoryId" type="number" />
<app-input label="تامین‌کننده" [control]="form.controls.supplierId" name="supplierId" type="number" /> -->
@@ -2,7 +2,7 @@
<div class="flex items-center gap-5 justify-between">
<div class="flex items-center gap-2">
<p-button icon="pi pi-arrow-right" aria-label="بازگشت" outlined [routerLink]="backRoute" />
<h3 class="m-0">{{ initialData ? "ویرایش محصول" : "ایجاد محصول جدید" }}</h3>
<h3 class="m-0">{{ initialData ? "ویرایش کالا" : "ایجاد کالا جدید" }}</h3>
</div>
<div class="flex items-center gap-2">
<button
@@ -19,7 +19,7 @@
</div>
<div class="flex gap-3">
<div class="flex flex-col gap-3 grow">
<p-card header="جزئیات محصول" class="w-full">
<p-card header="جزئیات کالا" class="w-full">
<form [formGroup]="form" (submit)="submit()" class="grid grid-cols-2 gap-4">
<app-input label="نام" [control]="form.controls.name" name="name" class="col-span-2" />
<app-input label="SKU" [control]="form.controls.sku" name="sku" />
@@ -1,9 +1,9 @@
<uikit-field label="محصول" [control]="control" [showLabel]="showLabel" [showErrors]="showErrors">
<uikit-field label="کالا" [control]="control" [showLabel]="showLabel" [showErrors]="showErrors">
<p-select
[options]="items()"
optionLabel="name"
[optionValue]="selectOptionValue"
placeholder="انتخاب محصول"
placeholder="انتخاب کالا"
[formControl]="control"
[showClear]="true"
[filter]="true"
@@ -1,8 +1,8 @@
import { Maybe } from '@/core';
import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit';
import { CommonModule } from '@angular/common';
import { Component, Input, signal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { Component } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Select } from 'primeng/select';
import { IProductResponse } from '../../models';
import { ProductsService } from '../../services/main.service';
@@ -12,20 +12,15 @@ import { ProductsService } from '../../services/main.service';
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;
export class ProductsSelectComponent extends AbstractSelectComponent<IProductResponse> {
constructor(private service: ProductsService) {
super();
this.getData();
}
loading = signal(false);
items = signal<Maybe<IProductResponse[]>>(null);
isOpenFormDialog = signal(false);
getLabel(): string {
return 'کالا';
}
getData() {
this.loading.set(true);
@@ -39,19 +34,4 @@ export class ProductsSelectComponent {
},
});
}
refresh() {
this.getData();
}
onOpenForm() {
// this.isOpenFormDialog.set(true);
}
get selectOptionValue() {
if (typeof this.control.value === 'number') {
return 'id';
}
return undefined;
}
}
@@ -8,7 +8,7 @@ export const productsNamedRoutes: NamedRoutes<TProductsRouteNames> = {
path: 'products',
loadComponent: () => import('../../views/list.component').then((m) => m.ProductsComponent),
meta: {
title: 'محصولات',
title: 'کالاها',
pagePath: () => '/products',
},
},
@@ -17,7 +17,7 @@ export const productsNamedRoutes: NamedRoutes<TProductsRouteNames> = {
loadComponent: () =>
import('../../views/create.component').then((m) => m.CreateProductComponent),
meta: {
title: 'ایجاد محصول جدید',
title: 'ایجاد کالا جدید',
pagePath: () => '/products/create',
},
},
@@ -25,7 +25,7 @@ export const productsNamedRoutes: NamedRoutes<TProductsRouteNames> = {
path: 'products/:productId',
loadComponent: () => import('../../views/single.component').then((m) => m.ProductComponent),
meta: {
title: 'محصول',
title: 'کالا',
pagePath: () => '/products/:productId',
},
},
@@ -34,7 +34,7 @@ export const productsNamedRoutes: NamedRoutes<TProductsRouteNames> = {
loadComponent: () =>
import('../../views/update.component').then((m) => m.UpdateProductComponent),
meta: {
title: 'محصول',
title: 'کالا',
pagePath: () => '/products/:productId/update',
},
},
@@ -43,7 +43,7 @@ export const productsNamedRoutes: NamedRoutes<TProductsRouteNames> = {
loadComponent: () =>
import('../../views/purchase.component').then((m) => m.ProductPurchaseComponent),
meta: {
title: 'محصول',
title: 'کالا',
pagePath: () => '/products/:productId/purchase',
},
},
@@ -25,7 +25,7 @@ export class CreateProductComponent {
this.submitLoading.set(true);
this.service.create(payload).subscribe({
next: (res) => {
this.toastService.success({ text: `محصول ${payload.name} با موفقیت ایجاد شد` });
this.toastService.success({ text: `کالا ${payload.name} با موفقیت ایجاد شد` });
this.router.navigate(['products', res.id]);
this.submitLoading.set(false);
},
@@ -1,10 +1,10 @@
<app-page-data-list
[pageTitle]="'مدیریت محصولات'"
[addNewCtaLabel]="'افزودن محصول جدید'"
[pageTitle]="'مدیریت کالاها'"
[addNewCtaLabel]="'افزودن کالا جدید'"
[columns]="columns"
[showAdd]="true"
emptyPlaceholderTitle="محصولی یافت نشد"
emptyPlaceholderDescription="برای افزودن محصول جدید، روی دکمهٔ بالا کلیک کنید."
emptyPlaceholderTitle="کالای یافت نشد"
emptyPlaceholderDescription="برای افزودن کالا جدید، روی دکمهٔ بالا کلیک کنید."
[items]="items()"
[loading]="loading()"
[showDetails]="true"
@@ -1,4 +1,4 @@
import { PurchaseReceiptTemplateComponent } from '@/modules/purchases/components/purchase-receipt-template.component';
import { PurchaseReceiptTemplateComponent } from '@/modules/purchases/components/receiptTemplate/purchase-receipt-template.component';
import { Component } from '@angular/core';
@Component({
@@ -45,7 +45,7 @@ export class UpdateProductComponent {
this.submitLoading.set(true);
this.service.update(this.productId, payload).subscribe({
next: (res) => {
this.toastService.success({ text: `محصول ${payload.name} با موفقیت ویرایش شد` });
this.toastService.success({ text: `کالا ${payload.name} با موفقیت ویرایش شد` });
this.router.navigate(['products', this.productId]);
this.submitLoading.set(false);
},
@@ -57,7 +57,7 @@ export class PurchaseFormComponent {
get formTitle() {
if (this.product) {
return `شارژ محصول ${this.product.name}`;
return `شارژ کالا ${this.product.name}`;
}
if (this.inventory) {
return `شارژ انبار ${this.inventory.name}`;
@@ -65,7 +65,7 @@ export class PurchaseFormComponent {
if (this.supplier) {
return `شارژ تامین‌کننده ${this.supplier.fullname}`;
}
return 'شارژ محصول';
return 'شارژ کالا';
}
constructor() {
@@ -0,0 +1 @@
export * from './receiptTemplate';
@@ -20,13 +20,13 @@
class="shrink-0"
(click)="removeProduct(i)"
[disabled]="productsControl.length === 1"
pTooltip="حذف محصول"
pTooltip="حذف کالا"
></button>
}
</div>
}
</div>
@if (canAddOrRemoveProducts) {
<button pButton type="button" label="افزودن محصول" (click)="addProduct()" pTooltip="افزودن محصول"></button>
<button pButton type="button" label="افزودن کالا" (click)="addProduct()" pTooltip="افزودن کالا"></button>
}
</div>
@@ -1,142 +0,0 @@
<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,2 @@
export * from './purchase-receipt-product-row.component';
export * from './purchase-receipt-template.component';
@@ -13,7 +13,12 @@
/>
</td>
<td>
<products-select-field [control]="purchaseItemControl.controls.product" [showLabel]="false" [showErrors]="false" />
<products-select-field
[control]="purchaseItemControl.controls.product"
[isFullDataOptionValue]="true"
[showLabel]="false"
[showErrors]="false"
/>
</td>
<td>
<textarea
@@ -6,7 +6,7 @@ import { ReactiveFormsModule } from '@angular/forms';
import { ButtonDirective } from 'primeng/button';
import { InputText } from 'primeng/inputtext';
import { Textarea } from 'primeng/textarea';
import { IPurchaseItemFormGroup } from '../models';
import { IPurchaseItemFormGroup } from '../../models';
@Component({
selector: 'tr[purchase-receipt-product-row]',
@@ -0,0 +1,181 @@
<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">
<div class="w-full flex items-center gap-2 h-10">
<span [class]="formIsSubmitted() && form.controls.inventory.invalid ? ' text-error' : 'text-muted-color'"
>انبار:</span
>
<shared-inline-edit>
<ng-template #data>
<span class="font-bold">{{ form.controls.inventory.value?.name || "وارد نشده" }}</span>
</ng-template>
<ng-template #field>
<inventories-select-field
[control]="form.controls.inventory"
[isFullDataOptionValue]="true"
[showErrors]="false"
[showLabel]="false"
/>
</ng-template>
</shared-inline-edit>
</div>
<div class="w-full flex items-center gap-2 h-10">
<span [class]="formIsSubmitted() && form.controls.supplier.invalid ? ' text-error' : 'text-muted-color'"
>تامین کننده:</span
>
<shared-inline-edit>
<ng-template #data>
<span class="font-bold">{{ form.controls.supplier.value?.fullname || "وارد نشده" }}</span>
</ng-template>
<ng-template #field>
<suppliers-select-field
[control]="form.controls.supplier"
[isFullDataOptionValue]="true"
[showErrors]="false"
[showLabel]="false"
/>
</ng-template>
</shared-inline-edit>
</div>
</div>
<div class="">
<div class="flex flex-col gap-2 max-w-xs items-end ms-auto w-full">
<div class="w-full flex items-center gap-2 h-10">
<span [class]="formIsSubmitted() && form.controls.code.invalid ? ' text-error' : 'text-muted-color'"
>شماره رسید:</span
>
<shared-inline-edit>
<ng-template #data>
<span class="font-bold">{{ form.controls.code.value || "وارد نشده" }}</span>
</ng-template>
<ng-template #field>
<app-input [control]="form.controls.code" [showErrors]="false" />
</ng-template>
</shared-inline-edit>
</div>
<div class="w-full flex items-center gap-2 h-10">
<span [class]="formIsSubmitted() && form.controls.buyAt.invalid ? ' text-error' : 'text-muted-color'"
>تاریخ رسید:</span
>
<shared-inline-edit>
<ng-template #data>
<span class="font-bold">{{ form.controls.buyAt.value || "وارد نشده" }}</span>
</ng-template>
<ng-template #field>
<uikit-datepicker [control]="form.controls.buyAt" [showErrors]="false" [showLabel]="false" />
</ng-template>
</shared-inline-edit>
</div>
</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>
<ng-template #footer>
<tr>
<td [attr.colspan]="8" class="p-0!">
<div class="flex justify-center p-3">
<button
pButton
type="button"
label="افزودن کالا"
icon="pi pi-plus"
(click)="onAddProductClick()"
></button>
</div>
</td>
</tr>
<tr>
<td [attr.colspan]="6" class="border-e border-surface-700"></td>
<td [attr.colspan]="2" class="p-0!">
<div class="flex flex-col gap-4 w-full border-s border-surface-700 px-3 py-6">
<div class="flex justify-between">
<span>جمع کل:</span>
<span class="font-bold" [appPriceMask]="totalAmount"></span>
</div>
<div class="flex justify-between">
<span>مالیات (۹٪):</span>
<span class="font-bold" [appPriceMask]="totalAmount * 0.09"></span>
</div>
</div>
</td>
</tr>
<tr>
<td [attr.colspan]="6" class="p-0! border-none!">
<div class="w-full flex items-center gap-2 py-10 border-surface-700 border-t-4">
<span>مبلغ قابل پرداخت:</span>
<span [appPriceAlphabet]="totalAmount + totalAmount * 0.09" class="font-bold text-lg"></span>
</div>
</td>
<td [attr.colspan]="2" class="p-0! border-none!">
<div class="flex h-full items-center justify-end py-10 px-2 border-surface-700 border-t-4 border-r">
<span [appPriceMask]="totalAmount + totalAmount * 0.09" class="text-xl font-bold"></span>
</div>
</td>
</tr>
</ng-template>
</p-table>
</p-card>
<div class="flex items-center justify-center gap-2 mt-10">
<button pButton type="button" label="لغو" outlined [disabled]="form.disabled" (click)="onCancel()"></button>
<button pButton type="button" label="ثبت رسید" [loading]="form.disabled" (click)="onSubmit()"></button>
</div>
</form>
@@ -1,18 +1,22 @@
import { Maybe } from '@/core';
import { ToastService } from '@/core/services/toast.service';
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 { InlineEditComponent, InputComponent } from '@/shared/components';
import { IColumn } from '@/shared/components/pageDataList/page-data-list.component';
import { PriceAlphabetDirective, PriceMaskDirective } from '@/shared/directives';
import { UikitFlatpickrJalaliComponent } from '@/uikit';
import { CommonModule } from '@angular/common';
import { Component, inject, Input } from '@angular/core';
import { Component, inject, Input, signal } 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 { IPurchaseRequest } from '../../models';
import { PurchasesService } from '../../services/main.service';
import { PurchaseReceiptProductRowComponent } from './purchase-receipt-product-row.component';
@Component({
@@ -27,13 +31,13 @@ import { PurchaseReceiptProductRowComponent } from './purchase-receipt-product-r
InputComponent,
TableModule,
InlineEditComponent,
KeyValueComponent,
PurchaseReceiptProductRowComponent,
PriceAlphabetDirective,
PriceMaskDirective,
ButtonDirective,
CommonModule,
ReactiveFormsModule,
UikitFlatpickrJalaliComponent,
],
})
export class PurchaseReceiptTemplateComponent {
@@ -43,15 +47,6 @@ export class PurchaseReceiptTemplateComponent {
@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',
@@ -96,25 +91,41 @@ export class PurchaseReceiptTemplateComponent {
] as IColumn[];
form = this.fb.group({
totalAmount: [0, [Validators.required, Validators.min(0)]],
code: ['', [Validators.required]],
totalAmount: [300000, [Validators.required, Validators.min(0)]],
code: ['122132', [Validators.required]],
isSettled: [false],
buyAt: ['', Validators.required],
buyAt: ['2025-12-01', 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)]],
product: [{ id: 1 } as Maybe<IProductResponse>, [Validators.required]],
count: [2, [Validators.required, Validators.min(1)]],
fee: [150000, [Validators.required, Validators.min(0)]],
description: [''],
total: [0, [Validators.required, Validators.min(0)]],
total: [300000, [Validators.required, Validators.min(0)]],
}),
]),
inventoryId: [this.inventory?.id || 0, [Validators.required]],
supplierId: [this.supplier?.id || 0, [Validators.required]],
inventory: [{ id: 2 } as Maybe<IInventoryResponse>, [Validators.required]],
supplier: [{ id: 1 } as Maybe<ISupplierResponse>, [Validators.required]],
});
constructor(
private service: PurchasesService,
private toastService: ToastService,
) {
this.form.controls.products.valueChanges.subscribe((products) => {
let totalAmount = 0;
products.forEach((p: any) => {
totalAmount += p.total || 0;
});
this.form.controls.totalAmount.setValue(totalAmount);
});
}
formIsSubmitted = signal(false);
ngOnInit() {
this.formIsSubmitted.set(false);
if (this.product) {
this.form.controls.products.at(0).setValue({
product: this.product,
@@ -147,9 +158,34 @@ export class PurchaseReceiptTemplateComponent {
}
onSubmit() {
this.formIsSubmitted.set(true);
this.form.markAllAsTouched();
if (this.form.valid) {
// Handle form submission logic here
console.log('Form Submitted', this.form.value);
const { products, inventory, supplier, ...res } = this.form.value;
const payload = {
...res,
inventoryId: this.form.controls.inventory.value?.id,
supplierId: this.form.controls.supplier.value?.id,
items: this.form.controls.products.value.map((item) => {
const { product, description, ...res } = item;
return { ...res, productId: item.product?.id };
}),
} as IPurchaseRequest;
console.log(payload);
this.form.disable();
this.service.create(payload).subscribe({
next: (res) => {
this.toastService.success({ text: 'ثبت سند خرید با موفقیت انجام شد' });
this.form.enable();
this.form.reset();
this.formIsSubmitted.set(false);
},
error: () => {
this.form.enable();
},
});
} else {
this.form.markAllAsTouched();
}
@@ -1,10 +1,7 @@
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 {}
@@ -1,9 +1,9 @@
<div class="">
<uikit-field label="تامین‌کننده">
<div class="w-full">
<uikit-field label="تامین‌کننده" [control]="control" [showLabel]="showLabel" [showErrors]="showErrors">
<p-select
[options]="items()"
optionLabel="fullname"
optionValue="id"
[optionValue]="selectOptionValue"
placeholder="انتخاب تامین‌کننده"
[formControl]="control"
[showClear]="true"
@@ -1,29 +1,21 @@
import { Maybe } from '@/core';
import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit';
import { Component, Input, signal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { Select } from 'primeng/select';
import { Component } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { SelectModule } 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],
imports: [ReactiveFormsModule, SelectModule, UikitFieldComponent],
})
export class SuppliersSelectComponent {
@Input() control!: FormControl<Maybe<number>>;
@Input() canInsert: boolean = false;
export class SuppliersSelectComponent extends AbstractSelectComponent<ISupplierResponse> {
constructor(private service: SuppliersService) {
super();
this.getData();
}
loading = signal(false);
items = signal<Maybe<ISupplierResponse[]>>(null);
isOpenFormDialog = signal(false);
getData() {
this.loading.set(true);
this.service.getAll().subscribe({
@@ -36,12 +28,4 @@ export class SuppliersSelectComponent {
},
});
}
refresh() {
this.getData();
}
onOpenForm() {
// this.isOpenFormDialog.set(true);
}
}
@@ -0,0 +1,44 @@
import { Maybe } from '@/core';
import { Component, Input, signal } from '@angular/core';
import { FormControl } from '@angular/forms';
export interface ISelectItem {
id: number;
name: string;
}
@Component({
selector: 'abstract-select-field',
template: '',
})
export abstract class AbstractSelectComponent<T = ISelectItem> {
@Input() control!: FormControl<Maybe<number | T>>;
@Input() canInsert: boolean = false;
@Input() showLabel: boolean = true;
@Input() showErrors: boolean = true;
@Input() isFullDataOptionValue: boolean = false;
@Input() optionValue = 'id';
loading = signal(false);
items = signal<Maybe<T[]>>(null);
isOpenFormDialog = signal(false);
abstract getData(): void;
// abstract getLabel(): string;
refresh() {
this.getData();
}
onOpenForm() {
// Override in subclass if needed
}
get selectOptionValue() {
if (!this.control) return undefined;
if (this.isFullDataOptionValue) {
return undefined;
}
return this.optionValue;
}
}
+1
View File
@@ -1,3 +1,4 @@
export * from './abstract-select.component';
export * from './breadcrumb.component';
export * from './card-data.component';
export * from './inlineConfirmation/inline-confirmation.component';
@@ -1,4 +1,10 @@
<uikit-field [label]="label" [control]="control" class="w-full datepicker">
<uikit-field
[label]="label"
[control]="control"
[showLabel]="showLabel"
[showErrors]="showErrors"
class="w-full datepicker"
>
<div #wrapper [attr.data-disable]="disabled" [attr.data-name]="name" class="w-full">
<input pInputText data-input [attr.placeholder]="placeholder" [name]="name" class="w-full" />
</div>
@@ -28,6 +28,8 @@ export class UikitFlatpickrJalaliComponent implements OnInit {
@Input() disabled = false;
@Input() name: string = 'datepicker';
@Input() options: Partial<BaseOptions> = {};
@Input() showLabel: boolean = true;
@Input() showErrors: boolean = true;
@Output() valueChange = new EventEmitter<string>();
+28 -2
View File
@@ -1,5 +1,5 @@
@import 'tailwindcss';
@import 'tailwindcss-primeui';
@import "tailwindcss";
@import "tailwindcss-primeui";
@custom-variant dark (&:where(.app-dark, .app-dark *));
@theme {
@@ -9,3 +9,29 @@
--breakpoint-xl: 1200px;
--breakpoint-2xl: 1920px;
}
@theme {
--color-primary-color: var(--primary-color);
--color-primary-contrast-color: var(--primary-contrast-color);
--color-text-color: var(--text-color);
--color-text-color-secondary: var(--text-color-secondary);
--color-surface-border: var(--surface-border);
--color-surface-card: var(--surface-card);
--color-surface-hover: var(--surface-hover);
--color-surface-overlay: var(--surface-overlay);
--color-transition-duration: var(--transition-duration);
--color-maskbg: var(--maskbg);
--color-content-border-radius: var(--content-border-radius);
--layout-section-transition-duration: 0.2s;
--color-element-transition-duration: var(--element-transition-duration);
--color-focus-ring-width: var(--focus-ring-width);
--color-focus-ring-style: var(--focus-ring-style);
--color-focus-ring-color: var(--focus-ring-color);
--color-focus-ring-offset: var(--focus-ring-offset);
--color-focus-ring-shadow: var(--focus-ring-shadow);
--color-surface-ground: var(--surface-ground);
--color-code-background: var(--code-background);
--color-code-color: var(--code-color);
--color-error: var(--p-red-500);
--p-inputtext-padding-y: 1rem !important;
}