feat: rename 'fee' to 'unitPrice' across purchase components and models

- Updated form component to use 'unitPrice' instead of 'fee'.
- Adjusted product charge row form fields and templates to reflect the new naming.
- Modified receipt template and related components to utilize 'unitPrice'.
- Changed models and interfaces to replace 'fee' with 'unitPrice'.
- Updated supplier invoice and statistics components to align with the new naming convention.
- Added new API routes for dashboard and statistics modules.
This commit is contained in:
2026-01-04 13:45:45 +03:30
parent 83c3d57866
commit 502c592f56
59 changed files with 582 additions and 105 deletions
@@ -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 +1,8 @@
<div class=""></div>
<app-page-data-list
[pageTitle]="'تراکنش‌های حساب‌های بانک'"
[columns]="columns"
emptyPlaceholderTitle="تراکنشی یافت نشد"
[items]="items()"
[loading]="loading()"
[showDetails]="true"
/>
@@ -1,9 +1,48 @@
import { Component } from '@angular/core';
import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { Component, signal } from '@angular/core';
import { IBankAccountsResponse } from '../models';
@Component({
selector: 'app-bank-account',
templateUrl: './single.component.html',
imports: [PageDataListComponent],
})
export class BankAccountComponent {
constructor() {}
columns = [
{ field: 'id', header: 'شناسه', width: '60px' },
{
field: 'name',
header: 'نام',
minWidth: '100px',
},
{
field: 'accountNumber',
header: 'شماره حساب بانکی',
canCopy: true,
minWidth: '160px',
},
{
field: 'iban',
header: 'شماره شبا',
canCopy: true,
minWidth: '200px',
},
{
field: 'cardNumber',
header: 'شماره کارت بانکی',
canCopy: true,
minWidth: '160px',
},
{ field: 'createdAt', header: 'تاریخ ایجاد', type: 'date' },
{ field: 'updatedAt', header: 'تاریخ به‌روزرسانی', type: 'date' },
] as IColumn[];
loading = signal(false);
items = signal<IBankAccountsResponse[]>([]);
}
@@ -58,7 +58,7 @@
</td>
<td class="text-center!">
@if (item.type === "IN") {
<span [appPriceMask]="item.fee"></span>
<span [appPriceMask]="item.unitPrice"></span>
} @else {
0
}
@@ -68,13 +68,13 @@
</td>
<td class="text-center!">
@if (item.type === "OUT") {
<span [appPriceMask]="item.fee"></span>
<span [appPriceMask]="item.unitPrice"></span>
} @else {
0
}
</td>
<td class="text-center!">{{ item.remainedInStock }}</td>
<!-- <td class="text-center!"><span [appPriceMask]="item.fee"></span></td>
<!-- <td class="text-center!"><span [appPriceMask]="item.unitPrice"></span></td>
<td class="text-center!"><span [appPriceMask]="item.totalCost"></span></td> -->
</tr>
</ng-template>
+1 -1
View File
@@ -4,7 +4,7 @@ export interface ICardexRawResponse {
id: number;
type: string;
quantity: number;
fee: number;
unitPrice: number;
totalCost: number;
referenceType: string;
referenceId: string;
@@ -0,0 +1,8 @@
const baseUrl = '/api/v1/pos';
export const POS_API_ROUTES = {
info: (posId: number) => `${baseUrl}/${posId}`,
stock: (posId: number) => `${baseUrl}/${posId}/stock`,
productCategories: (posId: number) => `${baseUrl}/${posId}/product-categories`,
submitOrder: (posId: number) => `${baseUrl}/${posId}/orders/create`,
};
@@ -0,0 +1,2 @@
export * from './apiRoutes';
export * from './routes';
@@ -0,0 +1,18 @@
import { NamedRoutes } from '@/core';
import { Routes } from '@angular/router';
export const dashboardNamedRoutes: NamedRoutes<any> = {
POS: {
path: '',
loadComponent: () =>
import('../../views/dashboard.component').then((m) => m.DashboardComponent),
meta: {
title: 'داشبورد',
pagePath: () => ``,
},
},
};
export type TDashboardRouteNames = keyof typeof dashboardNamedRoutes;
export const DASHBOARD_ROUTES: Routes = Object.values(dashboardNamedRoutes);
@@ -0,0 +1,7 @@
<div class="">
<div class="grid grid-cols-2 gap-4">
<app-statistics-shared-top-alert-stocks class="block h-[300px]" />
<app-statistics-shared-top-sales class="block h-[300px]" />
<app-statistics-shared-top-supplier-debts class="block h-[300px]" />
</div>
</div>
@@ -0,0 +1,36 @@
import { StatisticsSharedTopAlertStocksComponent } from '@/modules/statistics/shared/components/top-alert-stocks.component';
import { StatisticsSharedTopSalesComponent } from '@/modules/statistics/shared/components/top-sales.component';
import { StatisticsSharedTopSupplierDebtsComponent } from '@/modules/statistics/shared/components/top-supplier-debts.component';
import { Component } from '@angular/core';
import images from 'src/assets/images';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
imports: [
StatisticsSharedTopAlertStocksComponent,
StatisticsSharedTopSalesComponent,
StatisticsSharedTopSupplierDebtsComponent,
],
})
export class DashboardComponent {
placeholderLogo = images.placeholders.logo;
now = new Date();
// info = this.store.info;
// infoLoading = this.store.getInfoLoading;
// getInfo() {
// this.infoLoading.set(true);
// this.service.getInfo(Number(this.posId)).subscribe({
// next: (res) => {
// this.info.set(res);
// this.infoLoading.set(false);
// },
// error: () => {
// this.infoLoading.set(false);
// },
// });
// }
}
@@ -74,10 +74,10 @@
<p-sortIcon field="quantity" />
</div>
</th>
<th pSortableColumn="fee">
<th pSortableColumn="unitPrice">
<div class="flex items-center gap-2">
قیمت واحد
<p-sortIcon field="fee" />
<p-sortIcon field="unitPrice" />
</div>
</th>
<th pSortableColumn="totalCost">
@@ -94,7 +94,7 @@
<td>{{ movement.product.id }}</td>
<td>{{ movement.product.name }}</td>
<td>{{ movement.quantity }}</td>
<td><span [appPriceMask]="movement.fee"></span></td>
<td><span [appPriceMask]="movement.unitPrice"></span></td>
<td><span [appPriceMask]="movement.totalCost"></span></td>
<td>
<p-button type="button" icon="pi pi-search" [routerLink]="'/products/' + movement.product.id" />
@@ -5,7 +5,7 @@
[optionValue]="selectOptionValue"
placeholder="انتخاب انبار"
[formControl]="control"
[showClear]="true"
[showClear]="showClear"
[filter]="true"
appendTo="body"
(onChange)="change($event.value)"
+2 -2
View File
@@ -3,7 +3,7 @@ import { IProductRawResponse } from '@/modules/products/models';
export interface IInventoryMovement {
id: number;
quantity: number;
fee: number;
unitPrice: number;
totalCost: number;
avgCost: number;
product: IInventoryProduct;
@@ -27,7 +27,7 @@ export interface IInventoryInfo {
date: string;
type: string;
quantity: number;
fee: number;
unitPrice: number;
totalCost: number;
referenceType: string;
referenceId: string;
@@ -26,10 +26,10 @@
<p-sortIcon field="quantity" />
</div>
</th>
<th pSortableColumn="fee">
<th pSortableColumn="unitPrice">
<div class="flex items-center gap-2">
قیمت متوسط
<p-sortIcon field="fee" />
<p-sortIcon field="unitPrice" />
</div>
</th>
<th style="width: 4rem"></th>
@@ -80,10 +80,10 @@
<p-sortIcon field="quantity" />
</div>
</th>
<th pSortableColumn="fee">
<th pSortableColumn="unitPrice">
<div class="flex items-center gap-2">
قیمت متوسط
<p-sortIcon field="fee" />
<p-sortIcon field="unitPrice" />
</div>
</th>
<th style="width: 4rem"></th>
@@ -48,16 +48,17 @@
<div class="flex items-center gap-4 mt-auto">
<p-inplace>
<ng-template #display>
<span class="font-bold text-lg shrink-0" [appPriceMask]="inOrderProduct.fee"></span>
<span class="font-bold text-lg shrink-0" [appPriceMask]="inOrderProduct.unitPrice"></span>
</ng-template>
<ng-template #content let-closeCallback="closeCallback">
<p-input-number
type="text"
inputStyleClass="w-24"
[min]="0"
[(ngModel)]="inOrderProduct.fee"
[(ngModel)]="inOrderProduct.unitPrice"
(onBlur)="
updateInOrderProductFee(inOrderProduct.productId, $event, inOrderProduct.fee); closeCallback()
updateInOrderProductFee(inOrderProduct.productId, $event, inOrderProduct.unitPrice);
closeCallback()
"
/>
</ng-template>
@@ -46,11 +46,11 @@ export class PosOrderCardComponent {
}
updateInOrderProductFee(productId: number, $event: Event, prevFee: number) {
const fee = $event.target
const unitPrice = $event.target
? parseFloat(($event.target as HTMLInputElement).ariaValueNow || prevFee.toString())
: prevFee;
this.store.updateInOrderProductFee(productId, fee);
this.store.updateInOrderProductFee(productId, unitPrice);
}
clearOrderList() {
+1 -1
View File
@@ -16,7 +16,7 @@ interface ICategorySummary {
export interface IPosOrderItem {
productId: number;
count: number;
fee: number;
unitPrice: number;
}
export interface IPosInOrderProduct extends IPosOrderItem {
+5 -5
View File
@@ -72,7 +72,7 @@ export class POSStore {
readonly orderPricingInfo = computed(() => {
const totalAmount = this.inOrderProducts().reduce(
(acc, curr) => acc + curr.fee * curr.count,
(acc, curr) => acc + curr.unitPrice * curr.count,
0,
);
const taxAmount = totalAmount * 0.1;
@@ -188,7 +188,7 @@ export class POSStore {
product,
productId: product.id,
count: 1,
fee: parseFloat(product.salePrice),
unitPrice: parseFloat(product.salePrice),
maxQuantity: productStock.quantity,
},
],
@@ -220,13 +220,13 @@ export class POSStore {
}
}
updateInOrderProductFee(productId: number, fee: number) {
updateInOrderProductFee(productId: number, unitPrice: number) {
const inOrderProducts = this.state$().inOrderProducts;
if (inOrderProducts.some((p) => p.productId === productId)) {
const updatedProducts = inOrderProducts.map((p) => {
if (p.productId === productId) {
return { ...p, fee: fee };
return { ...p, unitPrice: unitPrice };
}
return p;
});
@@ -255,7 +255,7 @@ export class POSStore {
items: this.inOrderProducts()?.map((item) => ({
productId: item.productId,
count: item.count,
fee: item.fee,
unitPrice: item.unitPrice,
})),
};
this.service.submitOrder(this.posId, orderPayload).subscribe();
@@ -8,7 +8,7 @@
<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.unitPrice" />
<app-input label="مبلغ کل" [control]="form.controls.totalAmount" />
@@ -34,7 +34,7 @@ export class ProductChargeFormComponent {
form = this.fb.group({
count: [0, [Validators.required, Validators.min(1)]],
fee: [0, [Validators.required, Validators.min(0)]],
unitPrice: [0, [Validators.required, Validators.min(0)]],
totalAmount: [0, [Validators.required, Validators.min(0)]],
isSettled: [false],
buyAt: ['', Validators.required],
+2 -2
View File
@@ -2,7 +2,7 @@ export interface IProductChargeRaw {
id: number;
name: string;
count: number;
fee: number;
unitPrice: number;
totalAmount: number;
isSettled?: boolean;
buyAt: string;
@@ -20,7 +20,7 @@ export interface IProductChargeResponse extends IProductChargeRaw {}
export interface IProductChargeRequest {
name: string;
count: number;
fee: number;
unitPrice: number;
totalAmount: number;
isSettled?: boolean;
buyAt: string;
@@ -24,7 +24,7 @@ export class ProductChargesComponent {
{ field: 'id', header: 'شناسه' },
{ field: 'name', header: 'نام' },
{ field: 'count', header: 'تعداد' },
{ field: 'fee', header: 'هزینه' },
{ field: 'unitPrice', header: 'هزینه' },
{ field: 'totalAmount', header: 'مبلغ کل' },
{ field: 'isSettled', header: 'تسویه شده' },
{ field: 'buyAt', header: 'تاریخ خرید' },
@@ -48,7 +48,7 @@ export class PurchaseFormComponent {
this.fb.group({
productId: [this.product?.id || 0, [Validators.required]],
count: [0, [Validators.required, Validators.min(1)]],
fee: [0, [Validators.required, Validators.min(0)]],
unitPrice: [0, [Validators.required, Validators.min(0)]],
}),
]),
inventoryId: [this.inventory?.id || 0, [Validators.required]],
@@ -2,6 +2,6 @@
<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" />
<app-input label="قیمت" [control]="feeControl" name="unitPrice" />
</div>
</div>
@@ -6,7 +6,7 @@
<product-charge-row-form-fields
[productIdControl]="$any(product.get('productId'))"
[countControl]="$any(product.get('count'))"
[feeControl]="$any(product.get('fee'))"
[feeControl]="$any(product.get('unitPrice'))"
/>
</div>
@@ -25,7 +25,7 @@ export class ProductChargeRowFormComponent implements OnInit {
FormGroup<{
productId: FormControl<Maybe<number>>;
count: FormControl<Maybe<number>>;
fee: FormControl<Maybe<number>>;
unitPrice: FormControl<Maybe<number>>;
}>
>;
@Input() isSingleProduct = false;
@@ -43,7 +43,7 @@ export class ProductChargeRowFormComponent implements OnInit {
const productFormGroup = this.fb.group({
productId: [0, [Validators.required]],
count: [1, [Validators.required, Validators.min(1)]],
fee: [0, [Validators.required, Validators.min(0)]],
unitPrice: [0, [Validators.required, Validators.min(0)]],
});
this.productsControl.push(productFormGroup);
}
@@ -28,27 +28,27 @@
[autoResize]="false"
></textarea>
</td>
<td><app-input [control]="purchaseItemControl.controls.fee" [showErrors]="false" /></td>
<td><app-input [control]="purchaseItemControl.controls.unitPrice" [showErrors]="false" type="price" /></td>
<td><app-input [control]="purchaseItemControl.controls.count" /></td>
<td><span [appPriceMask]="purchaseItemControl.controls.total.value || 0"></span></td>
<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>
</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><span [appPriceMask]="purchaseItemControl.controls.unitPrice.value || 0"></span></td>
<td>{{ purchaseItemControl.controls.count.value }}</td>
<td><span [appPriceMask]="purchaseItemControl.controls.total.value || 0"></span></td>
<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>
</td> -->
}
@@ -34,12 +34,12 @@ export class PurchaseReceiptProductRowComponent {
ngOnInit() {
this.purchaseItemControl?.controls.product.valueChanges.subscribe((res) => {
this.purchaseItemControl?.controls.fee.setValue(Number(res?.salePrice) || 0);
this.purchaseItemControl?.controls.unitPrice.setValue(Number(res?.salePrice) || 0);
});
this.purchaseItemControl?.controls.count.valueChanges.subscribe(() => {
this.updateTotal();
});
this.purchaseItemControl?.controls.fee.valueChanges.subscribe(() => {
this.purchaseItemControl?.controls.unitPrice.valueChanges.subscribe(() => {
this.updateTotal();
});
}
@@ -47,7 +47,7 @@ export class PurchaseReceiptProductRowComponent {
updateTotal() {
this.purchaseItemControl.controls.total.setValue(
(this.purchaseItemControl.controls.count.value || 0) *
(this.purchaseItemControl.controls.fee.value || 0),
(this.purchaseItemControl.controls.unitPrice.value || 0),
);
}
@@ -9,38 +9,47 @@
<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>
<p-inplace [disabled]="!!inventory">
<ng-template #display>
<div class="flex items-center gap-2">
<span class="font-bold">{{ form.controls.inventory.value?.name || "وارد نشده" }}</span>
@if (!inventory) {
<i class="pi pi-pencil text-sm text-muted-color cursor-pointer"></i>
}
</div>
</ng-template>
<ng-template #field>
<ng-template #content let-closeCallback="closeCallback">
<inventories-select-field
[control]="form.controls.inventory"
[isFullDataOptionValue]="true"
[showErrors]="false"
[showLabel]="false"
[showClear]="false"
/>
</ng-template>
</shared-inline-edit>
</p-inplace>
</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>
<p-inplace>
<ng-template #display>
<div class="flex items-center gap-2">
<span class="font-bold">{{ form.controls.supplier.value?.fullname || "وارد نشده" }}</span>
<i class="pi pi-pencil text-sm text-muted-color cursor-pointer"></i>
</div>
</ng-template>
<ng-template #field>
<ng-template #content let-closeCallback="closeCallback">
<suppliers-select-field
[control]="form.controls.supplier"
[isFullDataOptionValue]="true"
[showErrors]="false"
[showLabel]="false"
[showClear]="false"
/>
</ng-template>
</shared-inline-edit>
</p-inplace>
</div>
</div>
<div class="">
@@ -49,27 +58,33 @@
<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>
<p-inplace>
<ng-template #display>
<div class="flex items-center gap-2">
<span class="font-bold">{{ form.controls.code.value || "وارد نشده" }}</span>
<i class="pi pi-pencil text-sm text-muted-color cursor-pointer"></i>
</div>
</ng-template>
<ng-template #field>
<app-input [control]="form.controls.code" [showErrors]="false" />
<ng-template #content let-closeCallback="closeCallback">
<app-input [control]="form.controls.code" [showErrors]="false" (onBlur)="closeCallback()" />
</ng-template>
</shared-inline-edit>
</p-inplace>
</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>
<p-inplace>
<ng-template #display>
<div class="flex items-center gap-2">
<span class="font-bold">{{ form.controls.buyAt.value || "وارد نشده" }}</span>
<i class="pi pi-pencil text-sm text-muted-color cursor-pointer"></i>
</div>
</ng-template>
<ng-template #field>
<ng-template #content let-closeCallback="closeCallback">
<uikit-datepicker [control]="form.controls.buyAt" [showErrors]="false" [showLabel]="false" />
</ng-template>
</shared-inline-edit>
</p-inplace>
</div>
</div>
</div>
@@ -116,7 +131,7 @@
</ng-template>
</shared-inline-edit>
} @else if (col.field === "total") {
{{ product["fee"] * product["count"] || 0 }}
{{ product["unitPrice"] * product["count"] || 0 }}
} @else if (col.field === "action") {
<div class="flex items-center gap-2">
<button></button>
@@ -130,7 +145,7 @@
</ng-template>
<ng-template #footer>
<tr>
<td [attr.colspan]="8" class="p-0!">
<td [attr.colspan]="7" class="p-0!">
<div class="flex justify-center p-3">
<button
pButton
@@ -143,7 +158,7 @@
</td>
</tr>
<tr>
<td [attr.colspan]="6" class="border-e border-surface-700"></td>
<td [attr.colspan]="5" 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">
@@ -158,7 +173,7 @@
</td>
</tr>
<tr>
<td [attr.colspan]="6" class="p-0! border-none!">
<td [attr.colspan]="5" 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.1" class="font-bold text-lg"></span>
@@ -180,6 +195,9 @@
</div>
@if (purchaseReceiptResponse()) {
<app-purchase-receipt-payment-wrapper [purchaseReceipt]="purchaseReceiptResponse()!" />
<app-purchase-receipt-payment-wrapper
[purchaseReceipt]="purchaseReceiptResponse()!"
(onSubmit)="paymentSubmitted()"
/>
}
</form>
@@ -5,7 +5,7 @@ import { IInventorySummaryResponse } 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 } from '@/shared/components';
import { InputComponent } from '@/shared/components';
import { IColumn } from '@/shared/components/pageDataList/page-data-list.component';
import { PurchaseReceiptPaymentWrapperComponent } from '@/shared/components/purchaseReceiptPayment/wrapper.component';
import { PriceAlphabetDirective, PriceMaskDirective } from '@/shared/directives';
@@ -16,6 +16,7 @@ import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import dayjs from 'dayjs';
import { ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card';
import { Inplace } from 'primeng/inplace';
import { TableModule } from 'primeng/table';
import { IPurchaseReceiptResponse, IPurchaseRequest } from '../../models';
import { PurchasesService } from '../../services/main.service';
@@ -32,7 +33,6 @@ import { PurchaseReceiptProductRowComponent } from './purchase-receipt-product-r
SuppliersSelectComponent,
InputComponent,
TableModule,
InlineEditComponent,
PurchaseReceiptProductRowComponent,
PriceAlphabetDirective,
PriceMaskDirective,
@@ -41,6 +41,7 @@ import { PurchaseReceiptProductRowComponent } from './purchase-receipt-product-r
ReactiveFormsModule,
UikitFlatpickrJalaliComponent,
PurchaseReceiptPaymentWrapperComponent,
Inplace,
],
})
export class PurchaseReceiptTemplateComponent {
@@ -65,6 +66,7 @@ export class PurchaseReceiptTemplateComponent {
field: 'name',
header: 'نام کالا',
minWidth: '160px',
width: '160px',
},
{
field: 'description',
@@ -72,9 +74,9 @@ export class PurchaseReceiptTemplateComponent {
width: '200px',
},
{
field: 'fee',
field: 'unitPrice',
header: 'قیمت واحد',
width: '120px',
width: '200px',
},
{
field: 'count',
@@ -82,15 +84,16 @@ export class PurchaseReceiptTemplateComponent {
width: '80px',
},
{
field: 'total',
field: 'totalPrice',
header: 'مبلغ کل',
type: 'price',
width: '140px',
},
{
field: 'action',
header: '',
width: '100px',
},
// {
// field: 'action',
// header: '',
// width: '100px',
// },
] as IColumn[];
form = this.fb.group({
@@ -104,7 +107,7 @@ export class PurchaseReceiptTemplateComponent {
this.fb.group({
product: [null as Maybe<IProductResponse>, [Validators.required]],
count: [0, [Validators.required, Validators.min(1)]],
fee: [0, [Validators.required, Validators.min(0)]],
unitPrice: [0, [Validators.required, Validators.min(0)]],
description: [''],
total: [0, [Validators.required, Validators.min(0)]],
}),
@@ -154,7 +157,7 @@ export class PurchaseReceiptTemplateComponent {
{
product: this.product,
count: 0,
fee: 0,
unitPrice: 0,
description: '',
total: 0,
},
@@ -171,7 +174,7 @@ export class PurchaseReceiptTemplateComponent {
this.fb.group({
product: [null as Maybe<IProductResponse>, [Validators.required]],
count: [0, [Validators.required, Validators.min(1)]],
fee: [0, [Validators.required, Validators.min(0)]],
unitPrice: [0, [Validators.required, Validators.min(0)]],
description: [''],
total: [0, [Validators.required, Validators.min(0)]],
}),
@@ -218,4 +221,10 @@ export class PurchaseReceiptTemplateComponent {
}
onCancel() {}
paymentSubmitted() {
this.purchaseReceiptResponse.set(null);
this.form.reset();
this.formIsSubmitted.set(false);
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ export * from './io';
export interface IPurchaseItemFormGroup extends FormGroup<{
product: FormControl<IProductResponse | null>;
count: FormControl<number>;
fee: FormControl<number>;
unitPrice: FormControl<number>;
description: FormControl<string>;
total: FormControl<number>;
}> {}
+3 -3
View File
@@ -17,7 +17,7 @@ export interface IPurchaseReceiptResponse extends IPurchaseReceiptRawResponse {}
export interface IPurchaseItemRawResponse {
id: number;
count: number;
fee: number;
unitPrice: number;
total: number;
productId: number;
}
@@ -34,7 +34,7 @@ export interface IPurchaseRequest {
export interface IPurchaseItemRequest {
productId: number;
count: number;
fee: number;
unitPrice: number;
total: number;
}
@@ -51,7 +51,7 @@ export interface IPurchaseForm {
export interface IPurchaseItemForm {
product: Maybe<IProductResponse>;
count: number;
fee: number;
unitPrice: number;
total: number;
description?: string;
}
@@ -0,0 +1,8 @@
const baseUrl = '/api/v1/pos';
export const POS_API_ROUTES = {
info: (posId: number) => `${baseUrl}/${posId}`,
stock: (posId: number) => `${baseUrl}/${posId}/stock`,
productCategories: (posId: number) => `${baseUrl}/${posId}/product-categories`,
submitOrder: (posId: number) => `${baseUrl}/${posId}/orders/create`,
};
@@ -0,0 +1 @@
export * from './apiRoutes';
@@ -0,0 +1,10 @@
<div class="card p-0! h-full!">
<div
class="flex items-center gap-2 justify-between p-4 border-b border-surface-border bg-surface-card sticky top-0 z-10"
>
<span class="text-lg font-bold">{{ title }}</span>
<button pButton icon="pi pi-refresh" outlined (click)="onRefresh.emit()" [loading]="loading"></button>
</div>
<div class="p-4 h-full overflow-auto"><ng-content></ng-content></div>
</div>
@@ -0,0 +1,17 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { ButtonDirective } from 'primeng/button';
@Component({
selector: 'app-statistics-shared-card',
templateUrl: './card.component.html',
imports: [ButtonDirective],
host: {
class: 'block h-full overflow-auto',
},
})
export class StatisticsSharedCardComponent {
@Input() title!: string;
@Input() loading: boolean = false;
@Output() onRefresh = new EventEmitter<void>();
constructor() {}
}
@@ -0,0 +1,12 @@
<app-statistics-shared-card title="لیست کالاهای رو به اتمام" [loading]="loading()" (onRefresh)="getData()">
<app-page-data-list
[columns]="column"
[items]="items()"
[loading]="loading()"
emptyPlaceholderDescription="در حال حاضر تمامی کالاها به مقدار کافی وجود دارند"
>
<ng-template #actionTpl let-item>
<button pButton icon="pi pi-cart-plus" outlined [routerLink]="['/products', item.id, 'purchase']"></button>
</ng-template>
</app-page-data-list>
</app-statistics-shared-card>
@@ -0,0 +1,66 @@
import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { Component, signal, TemplateRef, ViewChild } from '@angular/core';
import { RouterLink } from '@angular/router';
import { ButtonDirective } from 'primeng/button';
import { IStatisticsTopAlertStockResponse } from '../models/io';
import { SharedStatisticsService } from '../services/main.service';
import { StatisticsSharedCardComponent } from './card.component';
@Component({
selector: 'app-statistics-shared-top-alert-stocks',
templateUrl: './top-alert-stocks.component.html',
imports: [StatisticsSharedCardComponent, PageDataListComponent, ButtonDirective, RouterLink],
})
export class StatisticsSharedTopAlertStocksComponent {
items = signal<IStatisticsTopAlertStockResponse[]>([]);
loading = signal<boolean>(false);
column = [] as IColumn[];
@ViewChild('actionTpl', { static: true }) actionTpl!: TemplateRef<any>;
constructor(private readonly service: SharedStatisticsService) {
this.getData();
}
ngOnInit() {
this.column = [
{ field: 'sku', header: 'شناسه' },
{ field: 'name', header: 'عنوان' },
{
field: 'brand',
header: 'برند',
type: 'nested',
nestedPath: 'name',
},
{
field: 'stock',
header: 'موجودی ',
},
{ field: 'minimumStockAlertLevel', header: 'حد هشدار' },
{
field: 'chargeAction',
header: '',
customDataModel: this.actionTpl,
width: '40px',
},
];
}
getData() {
this.loading.set(true);
this.service.getTopAlertStock().subscribe({
next: (res) => {
this.items.set(res.data);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
}
toPurchase(item: IStatisticsTopAlertStockResponse) {}
}
@@ -0,0 +1,9 @@
<app-statistics-shared-card title="لیست پرفروش‌ترین کالاها" [loading]="loading()" (onRefresh)="getData()">
<app-page-data-list
[columns]="column"
[items]="items()"
[loading]="loading()"
emptyPlaceholderDescription="در حال حاضر فروشی انجام نشده است."
>
</app-page-data-list>
</app-statistics-shared-card>
@@ -0,0 +1,52 @@
import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { Component, signal, TemplateRef, ViewChild } from '@angular/core';
import { RouterLink } from '@angular/router';
import { ButtonDirective } from 'primeng/button';
import { IStatisticsTopSalesResponse } from '../models/io';
import { SharedStatisticsService } from '../services/main.service';
import { StatisticsSharedCardComponent } from './card.component';
@Component({
selector: 'app-statistics-shared-top-sales',
templateUrl: './top-sales.component.html',
imports: [StatisticsSharedCardComponent, PageDataListComponent, ButtonDirective, RouterLink],
})
export class StatisticsSharedTopSalesComponent {
items = signal<IStatisticsTopSalesResponse[]>([]);
loading = signal<boolean>(false);
column = [] as IColumn[];
@ViewChild('actionTpl', { static: true }) actionTpl!: TemplateRef<any>;
constructor(private readonly service: SharedStatisticsService) {
this.getData();
}
ngOnInit() {
this.column = [
{ field: 'sku', header: 'شناسه' },
{ field: 'name', header: 'عنوان' },
{
field: 'brand',
header: 'برند',
type: 'nested',
nestedPath: 'name',
},
];
}
getData() {
this.loading.set(true);
this.service.getTopSales().subscribe({
next: (res) => {
this.items.set(res.data);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
}
}
@@ -0,0 +1,9 @@
<app-statistics-shared-card title="لیست بدهی به تامین‌کنندگان" [loading]="loading()" (onRefresh)="getData()">
<app-page-data-list
[columns]="column"
[items]="items()"
[loading]="loading()"
emptyPlaceholderDescription="در حال حاضر بدهی به تامین‌کنندگان ندارید."
>
</app-page-data-list>
</app-statistics-shared-card>
@@ -0,0 +1,52 @@
import {
IColumn,
PageDataListComponent,
} from '@/shared/components/pageDataList/page-data-list.component';
import { Component, signal, TemplateRef, ViewChild } from '@angular/core';
import { IStatisticsTopSupplierDebtsResponse } from '../models/io';
import { SharedStatisticsService } from '../services/main.service';
import { StatisticsSharedCardComponent } from './card.component';
@Component({
selector: 'app-statistics-shared-top-supplier-debts',
templateUrl: './top-supplier-debts.component.html',
imports: [StatisticsSharedCardComponent, PageDataListComponent],
})
export class StatisticsSharedTopSupplierDebtsComponent {
items = signal<IStatisticsTopSupplierDebtsResponse[]>([]);
loading = signal<boolean>(false);
column = [] as IColumn[];
@ViewChild('actionTpl', { static: true }) actionTpl!: TemplateRef<any>;
constructor(private readonly service: SharedStatisticsService) {
this.getData();
}
ngOnInit() {
this.column = [
{ field: 'name', header: 'عنوان' },
{
field: 'totalDebt',
header: 'بدهی کل',
type: 'price',
},
{
field: 'unpaidReceipts',
header: 'فاکتورهای تسویه‌نشده',
},
];
}
getData() {
this.loading.set(true);
this.service.getTopSupplierDebts().subscribe({
next: (res) => {
this.items.set(res.data);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
}
}
@@ -0,0 +1,8 @@
const baseUrl = '/api/v1/statistics';
export const SHARED_STATISTICS_API_ROUTES = {
topAlertStock: `${baseUrl}/top-alert-stocks`,
topSales: `${baseUrl}/top-sales`,
topSupplierDebts: `${baseUrl}/top-supplier-debts`,
topSellProducts: `${baseUrl}/top-sell-products`,
};
@@ -0,0 +1 @@
export * from './apiRoutes';
@@ -0,0 +1 @@
export * from './io';
@@ -0,0 +1,27 @@
import ISummary from '@/core/models/summary';
export interface IStatisticsTopAlertStockRawResponse {
id: number;
name: string;
sku: string;
minimumStockAlertLevel: string;
stockBalances: any[];
category: ISummary;
brand: ISummary;
deficit: number;
}
export interface IStatisticsTopAlertStockResponse extends IStatisticsTopAlertStockRawResponse {}
export interface IStatisticsTopSalesRawResponse {}
export interface IStatisticsTopSalesResponse extends IStatisticsTopSalesRawResponse {}
export interface IStatisticsTopSupplierDebtsRawResponse {
id: number;
name: string;
totalDebt: number;
unpaidReceipts: number;
}
export interface IStatisticsTopSupplierDebtsResponse extends IStatisticsTopSupplierDebtsRawResponse {}
export interface IStatisticsTopSellProductsRawResponse {}
export interface IStatisticsTopSellProductsResponse extends IStatisticsTopSellProductsRawResponse {}
@@ -0,0 +1,46 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { SHARED_STATISTICS_API_ROUTES } from '../constants';
import {
IStatisticsTopAlertStockRawResponse,
IStatisticsTopAlertStockResponse,
IStatisticsTopSalesRawResponse,
IStatisticsTopSalesResponse,
IStatisticsTopSellProductsRawResponse,
IStatisticsTopSellProductsResponse,
IStatisticsTopSupplierDebtsRawResponse,
IStatisticsTopSupplierDebtsResponse,
} from '../models';
@Injectable({ providedIn: 'root' })
export class SharedStatisticsService {
constructor(private http: HttpClient) {}
private apiRoutes = SHARED_STATISTICS_API_ROUTES;
getTopAlertStock(): Observable<IPaginatedResponse<IStatisticsTopAlertStockResponse>> {
return this.http.get<IPaginatedResponse<IStatisticsTopAlertStockRawResponse>>(
`${this.apiRoutes.topAlertStock}`,
);
}
getTopSales(): Observable<IPaginatedResponse<IStatisticsTopSalesResponse>> {
return this.http.get<IPaginatedResponse<IStatisticsTopSalesRawResponse>>(
`${this.apiRoutes.topSales}`,
);
}
getTopSupplierDebts(): Observable<IPaginatedResponse<IStatisticsTopSupplierDebtsResponse>> {
return this.http.get<IPaginatedResponse<IStatisticsTopSupplierDebtsRawResponse>>(
`${this.apiRoutes.topSupplierDebts}`,
);
}
getTopSellProducts(): Observable<IPaginatedResponse<IStatisticsTopSellProductsResponse>> {
return this.http.get<IPaginatedResponse<IStatisticsTopSellProductsRawResponse>>(
`${this.apiRoutes.topSellProducts}`,
);
}
}
@@ -85,10 +85,10 @@
<p-sortIcon field="quantity" />
</div>
</th>
<th pSortableColumn="fee">
<th pSortableColumn="unitPrice">
<div class="flex items-center gap-2">
قیمت واحد
<p-sortIcon field="fee" />
<p-sortIcon field="unitPrice" />
</div>
</th>
<th pSortableColumn="totalCost">
@@ -105,7 +105,7 @@
<td>{{ item.product.id }}</td>
<td>{{ item.product.name }}</td>
<td>{{ item.count }}</td>
<td><span [appPriceMask]="item.fee"></span></td>
<td><span [appPriceMask]="item.unitPrice"></span></td>
<td><span [appPriceMask]="item.total"></span></td>
<td>
<p-button type="button" icon="pi pi-search" [routerLink]="'/products/' + item.product.id" />
@@ -6,7 +6,7 @@
[optionValue]="selectOptionValue"
placeholder="انتخاب تامین‌کننده"
[formControl]="control"
[showClear]="true"
[showClear]="showClear"
[filter]="true"
appendTo="body"
>
+1 -1
View File
@@ -13,7 +13,7 @@ export interface ISupplierInventorySummary {
export interface ISupplierReceiptItemSummary {
id: number;
count: string;
fee: string;
unitPrice: string;
total: string;
product: {
id: string;
@@ -44,7 +44,7 @@
</td>
<td class="text-center!">
@if (item.type === "IN") {
<span [appPriceMask]="item.fee"></span>
<span [appPriceMask]="item.unitPrice"></span>
} @else {
0
}
@@ -54,13 +54,13 @@
</td>
<td class="text-center!">
@if (item.type === "OUT") {
<span [appPriceMask]="item.fee"></span>
<span [appPriceMask]="item.unitPrice"></span>
} @else {
0
}
</td>
<td class="text-center!">{{ item.remainedInStock }}</td>
<!-- <td class="text-center!"><span [appPriceMask]="item.fee"></span></td>
<!-- <td class="text-center!"><span [appPriceMask]="item.unitPrice"></span></td>
<td class="text-center!"><span [appPriceMask]="item.totalCost"></span></td> -->
</tr>
</ng-template>
@@ -16,6 +16,7 @@
[required]="isRequired"
[invalid]="control.invalid && (control.touched || control.dirty)"
(input)="onInput($event)"
(onBlur)="blur.emit()"
/>
<p-inputgroup-addon>ریال</p-inputgroup-addon>
</p-inputgroup>
@@ -34,6 +35,7 @@
[required]="isRequired"
[invalid]="control.invalid && (control.touched || control.dirty)"
(input)="onInput($event)"
(onBlur)="blur.emit()"
/>
}
@if (hint) {
@@ -45,6 +45,7 @@ export class InputComponent {
@Input() showErrors = false;
@Input() hint?: string;
@Output() valueChange = new EventEmitter<string | number>();
@Output() blur = new EventEmitter<void>();
onInput(ev: Event) {
const v = (ev.target as HTMLInputElement).value;
@@ -5,5 +5,6 @@
[supplierId]="purchaseReceipt.supplierId.toString()"
[inventoryId]="purchaseReceipt.inventoryId.toString()"
[invoiceId]="purchaseReceipt.id.toString()"
(onSubmit)="submitted()"
/>
}
@@ -1,6 +1,6 @@
import { ToastService } from '@/core/services/toast.service';
import { SupplierInvoicePayFormComponent } from '@/modules/suppliers/components/invoices/pay-form.component';
import { Component, Input, signal } from '@angular/core';
import { Component, EventEmitter, Input, Output, signal } from '@angular/core';
import { IPurchaseReceiptResponse } from '../../../modules/purchases/models';
import { ConfirmationDialogService } from '../confirmationDialog/confirmation-dialog.service';
@@ -12,6 +12,8 @@ import { ConfirmationDialogService } from '../confirmationDialog/confirmation-di
export class PurchaseReceiptPaymentWrapperComponent {
@Input() purchaseReceipt!: IPurchaseReceiptResponse;
@Output() onSubmit = new EventEmitter<void>();
showPaymentForm = signal(false);
constructor(
private confirmService: ConfirmationDialogService,
@@ -40,7 +42,12 @@ export class PurchaseReceiptPaymentWrapperComponent {
});
}
submit() {}
submitted() {
console.log('submitted');
this.showPaymentForm.set(false);
this.onSubmit.emit();
}
toPaymentForm() {
this.showPaymentForm.set(true);