feat: Enhance customer and inventory components with new features and improvements

This commit is contained in:
2025-12-15 18:00:45 +03:30
parent 17fa65407c
commit 108d192f88
24 changed files with 207 additions and 78 deletions
@@ -1,4 +1,11 @@
<p-dialog header="فرم مشتری" [(visible)]="visible" [modal]="true" [style]="{ width: '600px' }" [closable]="true"> <p-dialog
header="فرم مشتری"
[(visible)]="visible"
[modal]="true"
[style]="{ width: '600px', zIndex: '100000' }"
appendTo="body"
[closable]="true"
>
<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.firstName" name="firstName" /> <app-input label="نام" [control]="form.controls.firstName" name="firstName" />
<app-input label="نام خانوادگی" [control]="form.controls.lastName" name="lastName" /> <app-input label="نام خانوادگی" [control]="form.controls.lastName" name="lastName" />
@@ -1,7 +1,8 @@
<uikit-field label="مشتری" [control]="control" [showLabel]="showLabel" [showErrors]="showErrors"> <uikit-field label="مشتری" [control]="control" [showLabel]="showLabel" [showErrors]="showErrors">
<p-select <p-select
[loading]="loading()"
[options]="items()" [options]="items()"
optionLabel="name" optionLabel="fullName"
[optionValue]="selectOptionValue" [optionValue]="selectOptionValue"
placeholder="انتخاب مشتری" placeholder="انتخاب مشتری"
[formControl]="control" [formControl]="control"
@@ -9,5 +10,21 @@
[filter]="true" [filter]="true"
appendTo="body" appendTo="body"
> >
@if (canInsert) {
<ng-template #footer>
<div class="p-3">
<p-button
label="افزودن مشتری جدید"
fluid
severity="secondary"
text
size="small"
icon="pi pi-plus"
(onClick)="onOpenForm()"
/>
</div>
</ng-template>
}
</p-select> </p-select>
<customer-form [(visible)]="isOpenFormDialog" (onSubmit)="refresh()" />
</uikit-field> </uikit-field>
@@ -2,14 +2,16 @@ import { AbstractSelectComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit'; import { UikitFieldComponent } from '@/uikit';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { Button } from 'primeng/button';
import { Select } from 'primeng/select'; import { Select } from 'primeng/select';
import { ICustomerResponse } from '../../models'; import { ICustomerResponse } from '../../models';
import { CustomersService } from '../../services/main.service'; import { CustomersService } from '../../services/main.service';
import { CustomerFormComponent } from '../form.component';
@Component({ @Component({
selector: 'customers-select-field', selector: 'customers-select-field',
templateUrl: './select.component.html', templateUrl: './select.component.html',
imports: [ReactiveFormsModule, Select, UikitFieldComponent], imports: [ReactiveFormsModule, Select, UikitFieldComponent, Button, CustomerFormComponent],
}) })
export class CustomersSelectComponent extends AbstractSelectComponent<ICustomerResponse> { export class CustomersSelectComponent extends AbstractSelectComponent<ICustomerResponse> {
constructor(private service: CustomersService) { constructor(private service: CustomersService) {
@@ -1,7 +1,8 @@
import { IPaginatedResponse } from '@/core/models/service.model'; import { IPaginatedResponse } from '@/core/models/service.model';
import { getFullName } from '@/utils';
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 { CUSTOMERS_API_ROUTES } from '../constants'; import { CUSTOMERS_API_ROUTES } from '../constants';
import { ICustomerRawResponse, ICustomerRequest, ICustomerResponse } from '../models'; import { ICustomerRawResponse, ICustomerRequest, ICustomerResponse } from '../models';
@@ -12,11 +13,22 @@ export class CustomersService {
private apiRoutes = CUSTOMERS_API_ROUTES; private apiRoutes = CUSTOMERS_API_ROUTES;
getAll(): Observable<IPaginatedResponse<ICustomerResponse>> { getAll(): Observable<IPaginatedResponse<ICustomerResponse>> {
return this.http.get<IPaginatedResponse<ICustomerRawResponse>>(this.apiRoutes.list()); return this.http.get<IPaginatedResponse<ICustomerRawResponse>>(this.apiRoutes.list()).pipe(
map((res) => {
return {
meta: res.meta,
data: res.data.map((item) => ({ ...item, fullName: getFullName(item) })),
};
}),
);
} }
getSingle(customerId: string): Observable<ICustomerResponse> { getSingle(customerId: string): Observable<ICustomerResponse> {
return this.http.get<ICustomerRawResponse>(this.apiRoutes.single(customerId)); return this.http.get<ICustomerRawResponse>(this.apiRoutes.single(customerId)).pipe(
map((res) => {
return { ...res, fullName: getFullName(res) };
}),
);
} }
create(data: ICustomerRequest): Observable<ICustomerResponse> { create(data: ICustomerRequest): Observable<ICustomerResponse> {
@@ -32,7 +32,9 @@
/> />
</td> </td>
<td>{{ movement.receiptId }}</td> <td>{{ movement.receiptId }}</td>
<td><catalog-movement-reference-tag [type]="movement.info.referenceType" /></td> <td>
<catalog-movement-reference-tag [type]="movement.info.referenceType" [movementType]="movement.info.type" />
</td>
<td>{{ movement.count }}</td> <td>{{ movement.count }}</td>
<td>{{ movement.info.quantity }}</td> <td>{{ movement.info.quantity }}</td>
<td><span [appPriceMask]="movement.info.totalCost"></span></td> <td><span [appPriceMask]="movement.info.totalCost"></span></td>
@@ -95,7 +95,6 @@
@if (!form.controls.fromInventory.value) { @if (!form.controls.fromInventory.value) {
<div class="text-center text-gray-500 py-10">لطفا انبار مبدا را انتخاب کنید</div> <div class="text-center text-gray-500 py-10">لطفا انبار مبدا را انتخاب کنید</div>
} @else { } @else {
{{ form.controls.items.value.length }}
<p-table [value]="form.controls.items.controls" class="w-full"> <p-table [value]="form.controls.items.controls" class="w-full">
<ng-template #header> <ng-template #header>
<tr> <tr>
@@ -107,13 +106,7 @@
<tr> <tr>
<td>{{ item.controls.product.value?.name }}</td> <td>{{ item.controls.product.value?.name }}</td>
<td> <td>
<input <app-uikit-counter [min]="1" [max]="10" [control]="item.controls.count" />
pInputText
type="number"
min="1"
[max]="item.controls.product.value?.quantity"
[formControl]="form.controls.items.at(rowIndex).controls.count"
/>
</td> </td>
</tr> </tr>
</ng-template> </ng-template>
@@ -1,6 +1,6 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import { InputComponent } from '@/shared/components'; import { InputComponent } from '@/shared/components';
import { UikitFieldComponent } from '@/uikit'; import { UikitCounterComponent, UikitFieldComponent } from '@/uikit';
import { Component, inject, signal } from '@angular/core'; import { Component, inject, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
@@ -32,6 +32,7 @@ import { InventoriesSelectComponent } from '../select/select.component';
TableModule, TableModule,
InputText, InputText,
ButtonDirective, ButtonDirective,
UikitCounterComponent,
], ],
}) })
export class InventoriesTransferFormComponent { export class InventoriesTransferFormComponent {
@@ -2,6 +2,7 @@ import { Maybe } from '@/core';
import { KeyValueComponent } from '@/shared/components'; import { KeyValueComponent } from '@/shared/components';
import { DetailsInfoCardComponent } from '@/shared/components/detailsInfoCard/details-info-card.component'; import { DetailsInfoCardComponent } from '@/shared/components/detailsInfoCard/details-info-card.component';
import { PriceMaskDirective } from '@/shared/directives'; import { PriceMaskDirective } from '@/shared/directives';
import { formatWithCurrency } from '@/utils';
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';
import { Button, ButtonDirective } from 'primeng/button'; import { Button, ButtonDirective } from 'primeng/button';
@@ -83,7 +84,7 @@ export class InventoryComponent {
{ {
icon: 'pi pi-dollar', icon: 'pi pi-dollar',
label: 'ارزش کلی کالاهای موجود', label: 'ارزش کلی کالاهای موجود',
value: this.data()?.availableProductsCost, value: formatWithCurrency(this.data()?.availableProductsCost || 0),
}, },
]; ];
} }
@@ -1,6 +1,11 @@
<div class="bg-surface-card w-md shrink-0 h-full overflow-hidden rounded-xl p-4 pb-0 flex flex-col"> <div class="bg-surface-card w-md shrink-0 h-full overflow-hidden rounded-xl p-4 pb-0 flex flex-col">
<div class="shrink-0 sticky top-0"> <div class="shrink-0 sticky top-0">
<customers-select-field [canInsert]="true" /> <customers-select-field
[canInsert]="true"
[value]="selectedCustomer()"
(valueChange)="updateCustomer($event)"
[isFullDataOptionValue]="true"
/>
</div> </div>
<hr /> <hr />
<div class="grow overflow-auto flex flex-col"> <div class="grow overflow-auto flex flex-col">
@@ -61,7 +66,7 @@
<app-uikit-counter <app-uikit-counter
[min]="1" [min]="1"
[max]="inOrderProduct.maxQuantity" [max]="inOrderProduct.maxQuantity"
[value]="inOrderProduct.quantity" [value]="inOrderProduct.count"
(valueChange)="updateInOrderProductQuantity(inOrderProduct.productId, $event)" (valueChange)="updateInOrderProductQuantity(inOrderProduct.productId, $event)"
></app-uikit-counter> ></app-uikit-counter>
</div> </div>
@@ -76,7 +81,15 @@
<div class="flex flex-col sticky bottom-0 py-2"> <div class="flex flex-col sticky bottom-0 py-2">
<pos-order-price-info-card /> <pos-order-price-info-card />
<div class="grid grid-cols grid-cols-2 sticky bottom-0 gap-2 pt-4"> <div class="grid grid-cols grid-cols-2 sticky bottom-0 gap-2 pt-4">
<button pButton type="button" label="ثبت سفارش" severity="primary" icon="pi pi-check" class="w-full"></button> <button
pButton
type="button"
label="ثبت سفارش"
severity="primary"
icon="pi pi-check"
class="w-full"
(click)="submit()"
></button>
<button pButton type="button" label="لغو" outlined icon="pi pi-times" class="w-full"></button> <button pButton type="button" label="لغو" outlined icon="pi pi-times" class="w-full"></button>
</div> </div>
</div> </div>
@@ -1,4 +1,6 @@
import { Maybe } from '@/core';
import { CustomersSelectComponent } from '@/modules/customers/components/select/select.component'; import { CustomersSelectComponent } from '@/modules/customers/components/select/select.component';
import { ICustomerResponse } from '@/modules/customers/models';
import { PriceMaskDirective } from '@/shared/directives'; import { PriceMaskDirective } from '@/shared/directives';
import { UikitCounterComponent } from '@/uikit'; import { UikitCounterComponent } from '@/uikit';
import { Component, computed, signal } from '@angular/core'; import { Component, computed, signal } from '@angular/core';
@@ -25,13 +27,15 @@ import { POSOrderPriceInfoCardComponent } from './price-info-card.component';
], ],
}) })
export class PosOrderCardComponent { export class PosOrderCardComponent {
constructor(private store: POSStore) {} constructor(private store: POSStore) {
this.selectedCustomer.set(this.store.selectedCustomer());
}
placeholderImage = images.placeholders.default; placeholderImage = images.placeholders.default;
inOrderProducts = computed(() => this.store.inOrderProducts()); inOrderProducts = computed(() => this.store.inOrderProducts());
value = signal(0); selectedCustomer = signal<Maybe<ICustomerResponse>>(null);
removeProductFromOrder(productId: number) { removeProductFromOrder(productId: number) {
this.store.removeFromInOrderProducts(productId); this.store.removeFromInOrderProducts(productId);
@@ -41,8 +45,10 @@ export class PosOrderCardComponent {
this.store.updateInOrderProductQuantity(productId, quantity); this.store.updateInOrderProductQuantity(productId, quantity);
} }
updateInOrderProductFee(productId: number, $event: Event, prevFee: string) { updateInOrderProductFee(productId: number, $event: Event, prevFee: number) {
const fee = $event.target ? String(($event.target as HTMLInputElement).ariaValueNow) : prevFee; const fee = $event.target
? parseFloat(($event.target as HTMLInputElement).ariaValueNow || prevFee.toString())
: prevFee;
this.store.updateInOrderProductFee(productId, fee); this.store.updateInOrderProductFee(productId, fee);
} }
@@ -50,4 +56,12 @@ export class PosOrderCardComponent {
clearOrderList() { clearOrderList() {
this.store.resetInOrderProducts(); this.store.resetInOrderProducts();
} }
updateCustomer(customer: Maybe<ICustomerResponse>) {
this.store.setSelectedCustomer(customer);
}
submit() {
this.store.submitOrder();
}
} }
@@ -1,15 +1,22 @@
<p-card class="border border-surface-border"> <p-card class="border border-surface-border">
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<div class="flex justify-between"> <div class="flex justify-between items-center">
<span>جمع قیمت</span> <span>جمع قیمت</span>
@if (priceInfo().totalAmount == priceInfo().totalBaseAmount) {
<span [appPriceMask]="priceInfo().totalAmount"></span>
} @else {
<div class="flex flex-col items-end">
<span class="line-through text-muted-color text-xs" [appPriceMask]="priceInfo().totalBaseAmount"></span>
<span [appPriceMask]="priceInfo().totalAmount"></span> <span [appPriceMask]="priceInfo().totalAmount"></span>
</div> </div>
<div class="flex justify-between"> }
</div>
<div class="flex justify-between items-center">
<span>مالیات (۱۰٪)</span> <span>مالیات (۱۰٪)</span>
<span [appPriceMask]="priceInfo().taxAmount"></span> <span [appPriceMask]="priceInfo().taxAmount"></span>
</div> </div>
<hr /> <hr />
<div class="flex justify-between font-bold text-lg"> <div class="flex justify-between items-center font-bold text-lg">
<span>مجموع کل</span> <span>مجموع کل</span>
<span [appPriceMask]="priceInfo().payableAmount"></span> <span [appPriceMask]="priceInfo().payableAmount"></span>
</div> </div>
@@ -4,4 +4,5 @@ export const POS_API_ROUTES = {
info: () => `${baseUrl}`, info: () => `${baseUrl}`,
stock: () => `${baseUrl}/stock`, stock: () => `${baseUrl}/stock`,
productCategories: () => `${baseUrl}/product-categories`, productCategories: () => `${baseUrl}/product-categories`,
submitOrder: () => `${baseUrl}/orders/create`,
}; };
+2 -2
View File
@@ -13,8 +13,8 @@ interface ICategorySummary {
export interface IPosOrderItem { export interface IPosOrderItem {
productId: number; productId: number;
quantity: number; count: number;
fee: string; fee: number;
} }
export interface IPosInOrderProduct extends IPosOrderItem { export interface IPosInOrderProduct extends IPosOrderItem {
@@ -6,6 +6,7 @@ import { POS_API_ROUTES } from '../constants';
import { import {
IPosInfoRawResponse, IPosInfoRawResponse,
IPosInfoResponse, IPosInfoResponse,
IPosOrderRequest,
IPosProductCategoriesRawResponse, IPosProductCategoriesRawResponse,
IPosProductCategoriesResponse, IPosProductCategoriesResponse,
IPosStockRawResponse, IPosStockRawResponse,
@@ -31,4 +32,8 @@ export class PosService {
this.apiRoutes.productCategories(), this.apiRoutes.productCategories(),
); );
} }
submitOrder(payload: IPosOrderRequest): Observable<any> {
return this.http.post<any>(this.apiRoutes.submitOrder(), payload);
}
} }
+30 -8
View File
@@ -1,4 +1,5 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import { ICustomerResponse } from '@/modules/customers/models';
import { computed, Injectable, signal } from '@angular/core'; import { computed, Injectable, signal } from '@angular/core';
import { map } from 'rxjs'; import { map } from 'rxjs';
import { IPosInfoResponse, IPosProductCategoriesResponse, IPosStockResponse } from '../models'; import { IPosInfoResponse, IPosProductCategoriesResponse, IPosStockResponse } from '../models';
@@ -14,6 +15,7 @@ interface IPosState {
productCategories: Maybe<IPosProductCategoriesResponse[]>; productCategories: Maybe<IPosProductCategoriesResponse[]>;
activeProductCategory: Maybe<number>; activeProductCategory: Maybe<number>;
inOrderProducts: IPosInOrderProduct[]; inOrderProducts: IPosInOrderProduct[];
selectedCustomer: Maybe<ICustomerResponse>;
} }
export const INITIAL_POS_STATE: IPosState = { export const INITIAL_POS_STATE: IPosState = {
@@ -25,6 +27,7 @@ export const INITIAL_POS_STATE: IPosState = {
productCategories: null, productCategories: null,
activeProductCategory: null, activeProductCategory: null,
inOrderProducts: [], inOrderProducts: [],
selectedCustomer: null,
}; };
@Injectable({ providedIn: 'any' }) @Injectable({ providedIn: 'any' })
@@ -41,6 +44,7 @@ export class POSStore {
readonly productCategories = computed(() => this.state$().productCategories); readonly productCategories = computed(() => this.state$().productCategories);
readonly getProductCategoriesLoading = computed(() => this.state$().getProductCategoriesLoading); readonly getProductCategoriesLoading = computed(() => this.state$().getProductCategoriesLoading);
readonly activeProductCategory = computed(() => this.state$().activeProductCategory); readonly activeProductCategory = computed(() => this.state$().activeProductCategory);
readonly selectedCustomer = computed(() => this.state$().selectedCustomer);
readonly activatedCategoryProducts = computed(() => { readonly activatedCategoryProducts = computed(() => {
if (this.activeProductCategory() === 0) { if (this.activeProductCategory() === 0) {
return this.state$().stock; return this.state$().stock;
@@ -52,14 +56,14 @@ export class POSStore {
readonly orderPricingInfo = computed(() => { readonly orderPricingInfo = computed(() => {
const totalAmount = this.inOrderProducts().reduce( const totalAmount = this.inOrderProducts().reduce(
(acc, curr) => acc + parseFloat(curr.fee) * curr.quantity, (acc, curr) => acc + curr.fee * curr.count,
0, 0,
); );
const taxAmount = totalAmount * 0.1; const taxAmount = totalAmount * 0.1;
return { return {
totalItems: this.inOrderProducts().reduce((acc, curr) => acc + curr.quantity, 0), totalItems: this.inOrderProducts().reduce((acc, curr) => acc + curr.count, 0),
totalBaseAmount: this.inOrderProducts().reduce( totalBaseAmount: this.inOrderProducts().reduce(
(acc, curr) => acc + parseFloat(curr.product.salePrice) * curr.quantity, (acc, curr) => acc + parseFloat(curr.product.salePrice) * curr.count,
0, 0,
), ),
totalAmount, totalAmount,
@@ -156,7 +160,7 @@ export class POSStore {
if (this.state$().inOrderProducts.some((p) => p.productId === productId)) { if (this.state$().inOrderProducts.some((p) => p.productId === productId)) {
this.updateInOrderProductQuantity( this.updateInOrderProductQuantity(
productId, productId,
this.state$().inOrderProducts.find((p) => p.productId === productId)!.quantity + 1, this.state$().inOrderProducts.find((p) => p.productId === productId)!.count + 1,
); );
} else { } else {
this.setState({ this.setState({
@@ -165,8 +169,8 @@ export class POSStore {
{ {
product, product,
productId: product.id, productId: product.id,
quantity: 1, count: 1,
fee: product.salePrice, fee: parseFloat(product.salePrice),
maxQuantity: productStock.quantity, maxQuantity: productStock.quantity,
}, },
], ],
@@ -190,7 +194,7 @@ export class POSStore {
} else { } else {
const updatedProducts = inOrderProducts.map((p) => { const updatedProducts = inOrderProducts.map((p) => {
if (p.productId === productId) { if (p.productId === productId) {
return { ...p, quantity: Math.min(quantity, p.maxQuantity) }; return { ...p, count: Math.min(quantity, p.maxQuantity) };
} }
return p; return p;
}); });
@@ -198,7 +202,7 @@ export class POSStore {
} }
} }
updateInOrderProductFee(productId: number, fee: string) { updateInOrderProductFee(productId: number, fee: number) {
const inOrderProducts = this.state$().inOrderProducts; const inOrderProducts = this.state$().inOrderProducts;
if (inOrderProducts.some((p) => p.productId === productId)) { if (inOrderProducts.some((p) => p.productId === productId)) {
@@ -215,4 +219,22 @@ export class POSStore {
resetInOrderProducts() { resetInOrderProducts() {
this.setState({ inOrderProducts: [] }); this.setState({ inOrderProducts: [] });
} }
setSelectedCustomer(customer: Maybe<ICustomerResponse>) {
console.log(customer);
this.setState({ selectedCustomer: customer });
}
submitOrder() {
const orderPayload = {
customerId: this.selectedCustomer()?.id,
items: this.inOrderProducts()?.map((item) => ({
productId: item.productId,
count: item.count,
fee: item.fee,
})),
};
this.service.submitOrder(orderPayload).subscribe();
}
} }
@@ -2,7 +2,7 @@
header="فرم برند کالا" header="فرم برند کالا"
[(visible)]="visible" [(visible)]="visible"
[modal]="true" [modal]="true"
[style]="{ width: '500px' }" [style]="{ width: '500px', zIndex: '10000' }"
[closable]="true" [closable]="true"
appendTo="body" appendTo="body"
> >
@@ -28,19 +28,20 @@ export class PurchaseReceiptProductRowComponent {
@Output() onRemove = new EventEmitter<void>(); @Output() onRemove = new EventEmitter<void>();
editMode = signal(false); editMode = signal(true);
constructor() {} constructor() {}
ngOnInit() { ngOnInit() {
this.purchaseItemControl?.controls.product.valueChanges.subscribe((res) => {
this.purchaseItemControl?.controls.fee.setValue(Number(res?.salePrice) || 0);
});
this.purchaseItemControl?.controls.count.valueChanges.subscribe(() => { this.purchaseItemControl?.controls.count.valueChanges.subscribe(() => {
this.updateTotal(); this.updateTotal();
}); });
this.purchaseItemControl?.controls.fee.valueChanges.subscribe(() => { this.purchaseItemControl?.controls.fee.valueChanges.subscribe(() => {
this.updateTotal(); this.updateTotal();
}); });
console.log('purchase item control', this.purchaseItemControl);
} }
updateTotal() { updateTotal() {
@@ -6,9 +6,9 @@
<div class="grid grid-cols-2 gap-4 border-b border-surface-700 pb-4"> <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="grid grid-cols-1 gap-2">
<div class="w-full flex items-center gap-2 h-10"> <div class="w-full flex items-center gap-2 h-10">
<span [class]="formIsSubmitted() && form.controls.inventory.invalid ? ' text-error' : 'text-muted-color'" <span [class]="formIsSubmitted() && form.controls.inventory.invalid ? ' text-error' : 'text-muted-color'">
>انبار:</span انبار:
> </span>
<shared-inline-edit> <shared-inline-edit>
<ng-template #data> <ng-template #data>
<span class="font-bold">{{ form.controls.inventory.value?.name || "وارد نشده" }}</span> <span class="font-bold">{{ form.controls.inventory.value?.name || "وارد نشده" }}</span>
@@ -24,9 +24,9 @@
</shared-inline-edit> </shared-inline-edit>
</div> </div>
<div class="w-full flex items-center gap-2 h-10"> <div class="w-full flex items-center gap-2 h-10">
<span [class]="formIsSubmitted() && form.controls.supplier.invalid ? ' text-error' : 'text-muted-color'" <span [class]="formIsSubmitted() && form.controls.supplier.invalid ? ' text-error' : 'text-muted-color'">
>تامین کننده:</span تامین کننده:
> </span>
<shared-inline-edit> <shared-inline-edit>
<ng-template #data> <ng-template #data>
<span class="font-bold">{{ form.controls.supplier.value?.fullname || "وارد نشده" }}</span> <span class="font-bold">{{ form.controls.supplier.value?.fullname || "وارد نشده" }}</span>
@@ -46,9 +46,9 @@
<div class=""> <div class="">
<div class="flex flex-col gap-2 max-w-xs items-end ms-auto w-full"> <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"> <div class="w-full flex items-center gap-2 h-10">
<span [class]="formIsSubmitted() && form.controls.code.invalid ? ' text-error' : 'text-muted-color'" <span [class]="formIsSubmitted() && form.controls.code.invalid ? ' text-error' : 'text-muted-color'">
>شماره رسید:</span شماره رسید:
> </span>
<shared-inline-edit> <shared-inline-edit>
<ng-template #data> <ng-template #data>
<span class="font-bold">{{ form.controls.code.value || "وارد نشده" }}</span> <span class="font-bold">{{ form.controls.code.value || "وارد نشده" }}</span>
@@ -59,9 +59,9 @@
</shared-inline-edit> </shared-inline-edit>
</div> </div>
<div class="w-full flex items-center gap-2 h-10"> <div class="w-full flex items-center gap-2 h-10">
<span [class]="formIsSubmitted() && form.controls.buyAt.invalid ? ' text-error' : 'text-muted-color'" <span [class]="formIsSubmitted() && form.controls.buyAt.invalid ? ' text-error' : 'text-muted-color'">
>تاریخ رسید:</span تاریخ رسید:
> </span>
<shared-inline-edit> <shared-inline-edit>
<ng-template #data> <ng-template #data>
<span class="font-bold">{{ form.controls.buyAt.value || "وارد نشده" }}</span> <span class="font-bold">{{ form.controls.buyAt.value || "وارد نشده" }}</span>
@@ -12,6 +12,7 @@ import { UikitFlatpickrJalaliComponent } from '@/uikit';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, inject, Input, signal } from '@angular/core'; import { Component, inject, Input, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import dayjs from 'dayjs';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import { Card } from 'primeng/card'; import { Card } from 'primeng/card';
import { TableModule } from 'primeng/table'; import { TableModule } from 'primeng/table';
@@ -91,22 +92,22 @@ export class PurchaseReceiptTemplateComponent {
] as IColumn[]; ] as IColumn[];
form = this.fb.group({ form = this.fb.group({
totalAmount: [300000, [Validators.required, Validators.min(0)]], totalAmount: [0, [Validators.required, Validators.min(0)]],
code: ['122132', [Validators.required]], code: ['', [Validators.required]],
isSettled: [false], isSettled: [false],
buyAt: ['2025-12-01', Validators.required], buyAt: [dayjs().calendar('jalali').format('YYYY/MM/DD'), Validators.required],
description: [''], description: [''],
products: this.fb.array([ products: this.fb.array([
this.fb.group({ this.fb.group({
product: [{ id: 1 } as Maybe<IProductResponse>, [Validators.required]], product: [null as Maybe<IProductResponse>, [Validators.required]],
count: [2, [Validators.required, Validators.min(1)]], count: [0, [Validators.required, Validators.min(1)]],
fee: [150000, [Validators.required, Validators.min(0)]], fee: [0, [Validators.required, Validators.min(0)]],
description: [''], description: [''],
total: [300000, [Validators.required, Validators.min(0)]], total: [0, [Validators.required, Validators.min(0)]],
}), }),
]), ]),
inventory: [{ id: 2 } as Maybe<IInventorySummaryResponse>, [Validators.required]], inventory: [null as Maybe<IInventorySummaryResponse>, [Validators.required]],
supplier: [{ id: 1 } as Maybe<ISupplierResponse>, [Validators.required]], supplier: [null as Maybe<ISupplierResponse>, [Validators.required]],
}); });
constructor( constructor(
@@ -114,6 +115,8 @@ export class PurchaseReceiptTemplateComponent {
private toastService: ToastService, private toastService: ToastService,
) { ) {
this.form.controls.products.valueChanges.subscribe((products) => { this.form.controls.products.valueChanges.subscribe((products) => {
console.log('first');
let totalAmount = 0; let totalAmount = 0;
products.forEach((p: any) => { products.forEach((p: any) => {
totalAmount += p.total || 0; totalAmount += p.total || 0;
@@ -126,14 +129,17 @@ export class PurchaseReceiptTemplateComponent {
ngOnInit() { ngOnInit() {
this.formIsSubmitted.set(false); this.formIsSubmitted.set(false);
this.form.controls.products.controls.pop();
if (this.product) { if (this.product) {
this.form.controls.products.at(0).setValue({ this.form.controls.products.setValue([
{
product: this.product, product: this.product,
count: 0, count: 0,
fee: 0, fee: 0,
description: '', description: '',
total: 0, total: 0,
}); },
]);
} }
} }
@@ -1,5 +1,6 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { Tag } from 'primeng/tag'; import { Tag } from 'primeng/tag';
import { MovementType } from '../movementTypes';
import { MovementReferenceType } from './types'; import { MovementReferenceType } from './types';
import { getMovementReferenceTypeColor, getMovementReferenceTypeText } from './utils'; import { getMovementReferenceTypeColor, getMovementReferenceTypeText } from './utils';
@@ -10,10 +11,11 @@ import { getMovementReferenceTypeColor, getMovementReferenceTypeText } from './u
}) })
export class CatalogMovementReferenceTagComponent { export class CatalogMovementReferenceTagComponent {
@Input() type!: MovementReferenceType; @Input() type!: MovementReferenceType;
@Input() movementType?: MovementType;
constructor() {} constructor() {}
get text() { get text() {
return getMovementReferenceTypeText(this.type); return getMovementReferenceTypeText(this.type, this.movementType);
} }
get color() { get color() {
return getMovementReferenceTypeColor(this.type); return getMovementReferenceTypeColor(this.type);
@@ -1,7 +1,11 @@
import { TagSeverity } from '@/core/models/tag'; import { TagSeverity } from '@/core/models/tag';
import { MovementType } from '../movementTypes';
import { MovementReferenceType } from './types'; import { MovementReferenceType } from './types';
export const getMovementReferenceTypeText = (movementType: MovementReferenceType): string => { export const getMovementReferenceTypeText = (
movementType: MovementReferenceType,
type?: MovementType,
): string => {
switch (movementType) { switch (movementType) {
case MovementReferenceType.PURCHASE: case MovementReferenceType.PURCHASE:
return 'خرید'; return 'خرید';
@@ -10,6 +14,12 @@ export const getMovementReferenceTypeText = (movementType: MovementReferenceType
case MovementReferenceType.ADJUSTMENT: case MovementReferenceType.ADJUSTMENT:
return 'تعدیل'; return 'تعدیل';
case MovementReferenceType.INVENTORY_TRANSFER: case MovementReferenceType.INVENTORY_TRANSFER:
if (type === MovementType.IN) {
return 'دریافت از انبار دیگر';
} else {
return 'ارسال به انبار دیگر';
}
default:
return 'انتقال بین انبارها'; return 'انتقال بین انبارها';
} }
}; };
@@ -1,5 +1,5 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import { Component, Input, signal } from '@angular/core'; import { Component, Input, model, signal } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
export interface ISelectItem { export interface ISelectItem {
@@ -12,7 +12,7 @@ export interface ISelectItem {
template: '', template: '',
}) })
export abstract class AbstractSelectComponent<T = ISelectItem> { export abstract class AbstractSelectComponent<T = ISelectItem> {
@Input() control!: FormControl<Maybe<number | T>>; @Input() control = new FormControl<Maybe<number | T>>(null);
@Input() canInsert: boolean = false; @Input() canInsert: boolean = false;
@Input() showLabel: boolean = true; @Input() showLabel: boolean = true;
@Input() showErrors: boolean = true; @Input() showErrors: boolean = true;
@@ -22,20 +22,29 @@ export abstract class AbstractSelectComponent<T = ISelectItem> {
loading = signal(false); loading = signal(false);
items = signal<Maybe<T[]>>(null); items = signal<Maybe<T[]>>(null);
isOpenFormDialog = signal(false); isOpenFormDialog = signal(false);
value = model<Maybe<T>>(null);
constructor() {
if (this.value()) {
this.control.setValue(this.value());
}
this.control.valueChanges.subscribe((val) => {
this.value.set(val as Maybe<T>);
});
}
abstract getData(): void; abstract getData(): void;
// abstract getLabel(): string;
refresh() { refresh() {
this.getData(); this.getData();
} }
onOpenForm() { onOpenForm() {
// Override in subclass if needed this.isOpenFormDialog.set(true);
} }
get selectOptionValue() { get selectOptionValue() {
if (!this.control) return undefined;
if (this.isFullDataOptionValue) { if (this.isFullDataOptionValue) {
return undefined; return undefined;
} }
@@ -33,7 +33,9 @@
<tr> <tr>
<td class="text-center!">{{ i + 1 }}</td> <td class="text-center!">{{ i + 1 }}</td>
<td class="text-center!" [jalaliDate]="item.createdAt"></td> <td class="text-center!" [jalaliDate]="item.createdAt"></td>
<td class="text-center!"><catalog-movement-reference-tag [type]="item.referenceType" /></td> <td class="text-center!">
<catalog-movement-reference-tag [type]="item.referenceType" [movementType]="item.type" />
</td>
<td class="text-center!">{{ item.referenceId || "-" }}</td> <td class="text-center!">{{ item.referenceId || "-" }}</td>
<td class="text-center!">{{ item.supplier?.name || "-" }}</td> <td class="text-center!">{{ item.supplier?.name || "-" }}</td>
<td class="text-center!">{{ item.inventory.name || "-" }}</td> <td class="text-center!">{{ item.inventory.name || "-" }}</td>
@@ -10,6 +10,7 @@ import {
ViewChild, ViewChild,
} from '@angular/core'; } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { FormControl, ReactiveFormsModule } from '@angular/forms';
import dayjs from 'dayjs';
import flatpickr from 'flatpickr-wrap'; import flatpickr from 'flatpickr-wrap';
import { BaseOptions } from 'flatpickr-wrap/dist/types/options'; import { BaseOptions } from 'flatpickr-wrap/dist/types/options';
import { InputTextModule } from 'primeng/inputtext'; import { InputTextModule } from 'primeng/inputtext';
@@ -40,6 +41,7 @@ export class UikitFlatpickrJalaliComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.fpInstance = flatpickr(this.wrapperRef.nativeElement, { this.fpInstance = flatpickr(this.wrapperRef.nativeElement, {
...this.options, ...this.options,
now: dayjs().calendar('jalali').format('YYYY/MM/DD'),
altInputClass: 'p-inputtext w-full', altInputClass: 'p-inputtext w-full',
onChange: (selectedDates: Date[], dateStr: string) => { onChange: (selectedDates: Date[], dateStr: string) => {
this.valueChange.emit(dateStr); this.valueChange.emit(dateStr);