feat(cardex): implement cardex module with filters, templates, and service integration

This commit is contained in:
2025-12-23 20:35:20 +03:30
parent e937c994d7
commit 1373cc046d
23 changed files with 473 additions and 134 deletions
+3
View File
@@ -13,6 +13,7 @@ export interface BaseState<T = any> {
items?: T; items?: T;
meta?: Maybe<IPaginatedQuery>; meta?: Maybe<IPaginatedQuery>;
isRefreshing?: boolean; isRefreshing?: boolean;
initialized: boolean;
} }
/** /**
@@ -77,6 +78,7 @@ export abstract class BaseStore<T extends BaseState> {
readonly state: Signal<T>; readonly state: Signal<T>;
readonly loading: Signal<boolean>; readonly loading: Signal<boolean>;
readonly error: Signal<Maybe<string>>; readonly error: Signal<Maybe<string>>;
readonly initialized: Signal<boolean>;
// Observable for RxJS compatibility // Observable for RxJS compatibility
readonly state$: Observable<T>; readonly state$: Observable<T>;
@@ -89,6 +91,7 @@ export abstract class BaseStore<T extends BaseState> {
this.state = this._state.asReadonly(); this.state = this._state.asReadonly();
this.loading = computed(() => this._state().loading); this.loading = computed(() => this._state().loading);
this.error = computed(() => this._state().error); this.error = computed(() => this._state().error);
this.initialized = computed(() => this._state().initialized);
// Observable for RxJS compatibility // Observable for RxJS compatibility
this.state$ = this._stateSubject.asObservable(); this.state$ = this._stateSubject.asObservable();
@@ -85,6 +85,7 @@ export class GlobalStore extends BaseStore<GlobalState> {
constructor() { constructor() {
super({ super({
initialized: false,
loading: false, loading: false,
error: null, error: null,
sidebarCollapsed: false, sidebarCollapsed: false,
@@ -300,6 +301,7 @@ export class GlobalStore extends BaseStore<GlobalState> {
*/ */
reset(): void { reset(): void {
this.setState({ this.setState({
initialized: false,
loading: false, loading: false,
error: null, error: null,
sidebarCollapsed: false, sidebarCollapsed: false,
+2
View File
@@ -48,6 +48,7 @@ export class AuthStore extends BaseStore<AuthState> {
constructor() { constructor() {
super({ super({
initialized: false,
loading: false, loading: false,
error: null, error: null,
user: null, user: null,
@@ -385,6 +386,7 @@ export class AuthStore extends BaseStore<AuthState> {
*/ */
reset(): void { reset(): void {
this.setState({ this.setState({
initialized: false,
loading: false, loading: false,
error: null, error: null,
user: null, user: null,
@@ -1,11 +1,11 @@
<form [formGroup]="form" class="flex items-end gap-4"> <form [formGroup]="form" class="flex items-end gap-4">
<div class="grow grid grid-cols-4 gap-2"> <div class="grow grid grid-cols-4 gap-2">
<inventories-select-field [control]="form.controls.inventoryId" /> <inventories-select-field [control]="form.controls.inventoryId" (onChange)="onChangeInventory($event)" />
<products-select-field [control]="form.controls.productId" /> <products-select-field [control]="form.controls.productId" (onChange)="onChangeProduct($event)" />
<uikit-datepicker [control]="form.controls.startDate" /> <uikit-datepicker [control]="form.controls.startDate" />
<uikit-datepicker [control]="form.controls.endDate" /> <uikit-datepicker [control]="form.controls.endDate" />
</div> </div>
<div class="shrink-0"> <div class="shrink-0">
<button pButton>جستجو</button> <button pButton (click)="search()">جستجو</button>
</div> </div>
</form> </form>
@@ -1,10 +1,17 @@
import { Maybe } from '@/core';
import { InventoriesSelectComponent } from '@/modules/inventories/components'; import { InventoriesSelectComponent } from '@/modules/inventories/components';
import { ProductsSelectComponent } from '@/modules/products/components/select/select.component'; import { ProductsSelectComponent } from '@/modules/products/components/select/select.component';
import { UikitFlatpickrJalaliComponent } from '@/uikit'; import { UikitFlatpickrJalaliComponent } from '@/uikit';
import { Component, inject } from '@angular/core'; import { Component, EventEmitter, inject, Output } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { ButtonDirective } from 'primeng/button'; import { ButtonDirective } from 'primeng/button';
import {
ICardexInventorySummary,
ICardexProductSummary,
ICardexRequestPayload,
} from '../../models';
import { CardexStore } from '../../store/main.store';
@Component({ @Component({
selector: 'cardex-filters', selector: 'cardex-filters',
@@ -20,12 +27,32 @@ import { ButtonDirective } from 'primeng/button';
export class CardexFiltersComponent { export class CardexFiltersComponent {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private fb = inject(FormBuilder); private fb = inject(FormBuilder);
constructor() {} private store = inject(CardexStore);
@Output() onSearch = new EventEmitter<void>();
filters = {} as Partial<ICardexRequestPayload>;
constructor(private router: Router) {
this.filters = this.store.filters();
}
form = this.fb.group({ form = this.fb.group({
inventoryId: [Number(this.route.snapshot.queryParams['inventoryId']) || 0], inventoryId: [this.filters.inventoryId || 0],
productId: [Number(this.route.snapshot.queryParams['productId']) || 0], productId: [this.filters.productId || 0],
startDate: [this.route.snapshot.queryParams['startDate'] || ''], startDate: [this.filters.startDate || ''],
endDate: [this.route.snapshot.queryParams['endDate'] || ''], endDate: [this.filters.endDate || ''],
}); });
onChangeInventory(inventory: Maybe<ICardexInventorySummary>) {
this.store.setInventory(inventory);
}
onChangeProduct(product: Maybe<ICardexProductSummary>) {
this.store.setProduct(product);
}
search() {
this.store.updateFilter(this.form.value as ICardexRequestPayload);
this.store.getCardex();
}
} }
@@ -0,0 +1,73 @@
<p-table
stripedRows
[value]="items"
[loading]="loading"
showGridlines
tableStyleClass="table-fixed border-collapse"
scrollable
>
<ng-template #header>
<tr>
<th rowspan="2" [style]="{ textAlign: 'center', width: '4rem' }">ردیف</th>
<th rowspan="2" [style]="{ textAlign: 'center', width: '8rem' }">تاریخ</th>
<th rowspan="2" [style]="{ textAlign: 'center', width: '12rem' }">شرح</th>
<th rowspan="2" [style]="{ textAlign: 'center', width: '6rem' }">شماره</th>
<th rowspan="2" [style]="{ textAlign: 'center', minWidth: '14rem', width: '14rem' }">طرف حساب</th>
<th rowspan="2" [style]="{ textAlign: 'center', width: '12rem' }">کالا</th>
<th colspan="2" class="text-center!" [style]="{ width: '15rem' }">ورودی</th>
<th colspan="2" class="text-center!" [style]="{ width: '15rem' }">خروجی</th>
<th colspan="1" class="text-center!" [style]="{ width: '5rem' }">مانده</th>
</tr>
<tr>
<th [style]="{ textAlign: 'center', width: '5rem' }">تعداد</th>
<th [style]="{ textAlign: 'center', width: '10rem' }">فی</th>
<th [style]="{ textAlign: 'center', width: '5rem' }">تعداد</th>
<th [style]="{ textAlign: 'center', width: '10rem' }">فی</th>
<th [style]="{ textAlign: 'center', width: '5rem' }">تعداد</th>
<!-- <th [style]="{ textAlign: 'center', width: '10rem' }">فی</th>
<th [style]="{ textAlign: 'center', width: '10rem' }">مجموع قیمت</th> -->
</tr>
</ng-template>
<ng-template #body let-item let-i="rowIndex">
<tr>
<td class="text-center!">{{ i + 1 }}</td>
<td class="text-center!" [jalaliDate]="item.createdAt"></td>
<td class="text-center!">
<catalog-movement-reference-tag
[type]="item.referenceType"
[movementType]="item.type"
[counterName]="item.counterInventory?.name"
/>
</td>
<td class="text-center!">{{ item.referenceId || "-" }}</td>
<td class="text-center!">
{{ (item.referenceType === "SALES" ? item.customer?.name : item.supplier?.name) || "-" }}
</td>
<td class="text-center!">{{ item.product.name || "-" }}</td>
<td class="text-center!">
{{ item.type === "IN" ? item.quantity : 0 }}
</td>
<td class="text-center!">
@if (item.type === "IN") {
<span [appPriceMask]="item.fee"></span>
} @else {
0
}
</td>
<td class="text-center!">
{{ item.type === "OUT" ? item.quantity : 0 }}
</td>
<td class="text-center!">
@if (item.type === "OUT") {
<span [appPriceMask]="item.fee"></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.totalCost"></span></td> -->
</tr>
</ng-template>
</p-table>
@@ -0,0 +1,22 @@
import { CatalogMovementReferenceTagComponent } from '@/shared/catalog/movementReferenceTypes';
import { JalaliDateDirective, PriceMaskDirective } from '@/shared/directives';
import { Component, Input } from '@angular/core';
import { TableModule } from 'primeng/table';
import { IInventoryCardex } from './types';
@Component({
selector: 'cardex-templates-inventory',
templateUrl: './inventory.component.html',
imports: [
TableModule,
JalaliDateDirective,
CatalogMovementReferenceTagComponent,
PriceMaskDirective,
],
hostDirectives: [PriceMaskDirective],
})
export class CardexComponent {
@Input() items: IInventoryCardex[] = [];
@Input() loading: boolean = false;
constructor() {}
}
@@ -0,0 +1,3 @@
import { ICardexResponse } from '../../models';
export interface IInventoryCardex extends ICardexResponse {}
+37 -59
View File
@@ -1,66 +1,44 @@
import { import { ICardexInventorySummary, ICardexProductSummary } from './types';
IInventoryInfo,
IInventoryMovement,
IInventoryStockProduct,
IInventoryTransferRequestItem,
} from './types';
export interface IInventorySummaryRawResponse { export interface ICardexRawResponse {
id: number;
type: string;
quantity: number;
fee: number;
totalCost: number;
referenceType: string;
referenceId: string;
createdAt: string;
avgCost: number;
product: ICardexProductSummary;
inventory: ICardexInventorySummary;
supplier?: {
id: number;
firstName: string;
lastName: string;
};
customer?: {
id: number;
firstName: string;
lastName: string;
};
counterInventory?: ICardexInventorySummary;
}
export interface ICardexResponse extends ICardexRawResponse {
supplier?: {
id: number; id: number;
name: string; name: string;
location: string; };
isActive: boolean; customer?: {
isPointOfSale: boolean; id: number;
createdAt: string;
updatedAt: string;
deletedAt: Maybe<string>;
}
export interface IInventorySummaryResponse extends IInventorySummaryRawResponse {}
export interface IInventoryDetailRawResponse extends IInventorySummaryRawResponse {
availableProductTypes: number;
availableProductCount: number;
availableProductsCost: number;
}
export interface IInventoryDetailResponse extends IInventoryDetailRawResponse {}
export interface IInventoryRequest {
name: string; name: string;
location: string; };
isActive: boolean;
} }
export interface IInventoryStockMovementRawResponse { export interface ICardexRequestPayload {
receiptId: string; startDate?: string;
count: number; endDate?: string;
info: IInventoryInfo; inventoryId?: number;
movements: IInventoryMovement[]; productId?: number;
} }
export interface IInventoryStockMovementResponse extends IInventoryStockMovementRawResponse {}
export interface IInventoryTransferRequest {
code: string;
description: string;
fromInventoryId: number;
toInventoryId: number;
items: IInventoryTransferRequestItem[];
}
export interface IInventoryStockRawResponse {
quantity: number;
avgCost: number;
product: IInventoryStockProduct;
}
export interface IInventoryStockResponse extends IInventoryStockRawResponse {}
export interface IInventoryStockQuery {
isAvailable?: boolean;
}
export interface IInventoryProductCardexRawResponse extends IInventoryStockMovementRawResponse {}
export interface IInventoryProductCardexResponse extends IInventoryProductCardexRawResponse {}
+7 -39
View File
@@ -1,42 +1,10 @@
import { IProductRawResponse } from '@/modules/products/models'; export interface ICardexInventorySummary {
name: string;
export interface IInventoryMovement { id: number;
id: number; }
quantity: number;
fee: number; export interface ICardexProductSummary {
totalCost: number;
avgCost: number;
product: IInventoryProduct;
remainedInStock: number;
}
export interface IInventoryProduct {
id: number;
name: string; name: string;
description: null;
sku: string; sku: string;
barcode: string; id: number;
createdAt: string;
updatedAt: string;
deletedAt: null;
brandId: number;
categoryId: number;
} }
export interface IInventoryInfo {
date: string;
type: string;
quantity: number;
fee: number;
totalCost: number;
referenceType: string;
referenceId: string;
createdAt: string;
}
export interface IInventoryTransferRequestItem {
productId: number;
count: number;
}
export interface IInventoryStockProduct extends IProductRawResponse {}
@@ -0,0 +1,43 @@
import { IPaginatedResponse } from '@/core/models/service.model';
import { getFullName } from '@/utils';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { map, Observable } from 'rxjs';
import { CARDEX_API_ROUTES } from '../constants';
import { ICardexRawResponse, ICardexRequestPayload, ICardexResponse } from '../models';
@Injectable({ providedIn: 'root' })
export class CardexService {
constructor(private http: HttpClient) {}
private apiRoutes = CARDEX_API_ROUTES;
getCardex(params: ICardexRequestPayload): Observable<IPaginatedResponse<ICardexResponse>> {
return this.http
.get<IPaginatedResponse<ICardexRawResponse>>(this.apiRoutes.cardex(), {
params: { ...params },
})
.pipe(
map((res) => {
return {
meta: res.meta,
data: res.data.map((item) => ({
...item,
supplier: item.supplier
? {
...item.supplier,
name: getFullName(item.supplier),
}
: undefined,
customer: item.customer
? {
...item.customer,
name: getFullName(item.customer),
}
: undefined,
})),
};
}),
);
}
}
+187
View File
@@ -0,0 +1,187 @@
import { Maybe } from '@/core';
import { ToastService } from '@/core/services/toast.service';
import { PaginatedState, PaginatedStore } from '@/core/state';
import { computed, Injectable } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import {
ICardexInventorySummary,
ICardexProductSummary,
ICardexRequestPayload,
ICardexResponse,
} from '../models';
import { CardexService } from '../services/main.service';
export interface CardexState extends PaginatedState<ICardexResponse> {
filters: ICardexRequestPayload;
selectedInventory: Maybe<ICardexInventorySummary>;
selectedProduct: Maybe<ICardexProductSummary>;
variant: 'inventory' | 'product' | 'both';
canChangeVariant: boolean;
}
@Injectable({
providedIn: 'root',
})
export class CardexStore extends PaginatedStore<ICardexResponse, CardexState> {
private readonly _cardexState = {
isRefreshing: false,
filters: {},
selectedInventory: null,
selectedProduct: null,
variant: 'both',
canChangeVariant: true,
};
constructor(
private router: Router,
private activeRoute: ActivatedRoute,
private service: CardexService,
private toastService: ToastService,
) {
const queryParams = activeRoute.snapshot.queryParams;
super({
loading: false,
initialized: false,
error: null,
isRefreshing: false,
items: [],
totalCount: 0,
currentPage: Number(queryParams['page']) || 1,
pageSize: Number(queryParams['pageSize']) || 30,
totalPages: 0,
hasMore: false,
filters: {
inventoryId: queryParams['inventoryId'],
productId: queryParams['productId'],
startDate: queryParams['startDate'],
endDate: queryParams['endDate'],
},
selectedInventory: null,
selectedProduct: null,
variant: 'both',
canChangeVariant: true,
});
this.initial();
}
readonly filters = computed(() => this._state().filters);
readonly variant = computed(() => this._state().variant);
readonly canChangeVariant = computed(() => this._state().canChangeVariant);
readonly cardexTitle = computed(() => {
switch (this._state().variant) {
case 'product':
return `کاردکس کالای ${this._state().selectedProduct?.name}`;
case 'inventory':
return `کاردکس انبار ${this._state().selectedInventory?.name}`;
default:
return 'کاردکس';
}
});
/**
* Reset state to initial values
*/
reset(): void {
const queryParams = this.activeRoute.snapshot.queryParams;
this.setState({
loading: false,
initialized: false,
error: null,
isRefreshing: false,
items: [],
totalCount: 0,
currentPage: Number(queryParams['page']) || 1,
pageSize: Number(queryParams['pageSize']) || 30,
totalPages: 0,
hasMore: false,
filters: {
inventoryId: queryParams['inventoryId'],
productId: queryParams['productId'],
startDate: queryParams['startDate'],
endDate: queryParams['endDate'],
},
selectedInventory: null,
selectedProduct: null,
variant: 'both',
canChangeVariant: this.state().canChangeVariant,
});
}
initial() {
this.getCardex();
}
getCardex() {
const filters = this.validateFilters();
if (filters) {
this.patchState({ initialized: true });
this.setLoading(true);
this.service.getCardex(filters).subscribe({
next: (res) => {
this.setItems(res.data, res.meta);
},
error: (err) => {
this.setError(err);
},
});
}
}
private validateFilters() {
const { productId, inventoryId, startDate, endDate } = this.filters();
const filters = {} as Partial<ICardexRequestPayload>;
Object.entries(this.filters()).forEach(([key, value]) => {
if (value) {
filters[key as keyof ICardexRequestPayload] = value;
}
});
if (!productId && !inventoryId) {
this.toastService.warn({ text: 'برای مشاهده‌ی کاردکس حداقل انبار و یا کالا را مشخص کنید' });
return false;
}
return filters;
}
onPageChange(page: number) {
this.setCurrentPage(page);
this.getCardex();
}
updateFilter(filters: ICardexRequestPayload) {
this.updateVariant();
console.log(filters);
this.router.navigate([], {
queryParams: filters,
skipLocationChange: true,
});
this.patchState({ filters });
}
setProduct(product: Maybe<ICardexProductSummary>) {
this.patchState({ selectedProduct: product });
}
setInventory(inventory: Maybe<ICardexInventorySummary>) {
this.patchState({ selectedInventory: inventory });
}
setCanChangeVariant(status: boolean) {
this.patchState({ canChangeVariant: status });
}
private updateVariant() {
const inventoryIsSelected = Boolean(this._state().selectedInventory);
const productIsSelected = Boolean(this._state().selectedProduct);
let variant = 'both' as 'product' | 'inventory' | 'both';
if (inventoryIsSelected) {
if (!productIsSelected) {
variant = 'inventory';
}
} else if (productIsSelected) {
variant = 'product';
}
this.patchState({ variant });
}
}
@@ -3,6 +3,13 @@
<cardex-filters /> <cardex-filters />
</p-card> </p-card>
<p-card> <p-card>
@if (!initialized) {
<uikit-empty-state title="برای مشاهده‌ی کاردکس اطلاعات بالا را تکمیل کنید"> </uikit-empty-state> <uikit-empty-state title="برای مشاهده‌ی کاردکس اطلاعات بالا را تکمیل کنید"> </uikit-empty-state>
} @else {
<h3 class="mb-4">{{ cardexTitle }}</h3>
@if (variant === "inventory") {
<cardex-templates-inventory [items]="items" [loading]="loading" />
}
}
</p-card> </p-card>
</div> </div>
@@ -2,12 +2,30 @@ import { UikitEmptyStateComponent } from '@/uikit';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Card } from 'primeng/card'; import { Card } from 'primeng/card';
import { CardexFiltersComponent } from '../components'; import { CardexFiltersComponent } from '../components';
import { CardexComponent } from '../components/templates/inventory.component';
import { CardexStore } from '../store/main.store';
@Component({ @Component({
selector: 'app-cardex', selector: 'app-cardex',
templateUrl: './cardex.component.html', templateUrl: './cardex.component.html',
imports: [Card, CardexFiltersComponent, UikitEmptyStateComponent], imports: [Card, CardexFiltersComponent, UikitEmptyStateComponent, CardexComponent],
}) })
export class CardexPageComponent { export class CardexPageComponent {
constructor() {} constructor(private store: CardexStore) {}
get cardexTitle() {
return this.store.cardexTitle();
}
get initialized() {
return this.store.initialized();
}
get loading() {
return this.store.loading();
}
get variant() {
return this.store.variant();
}
get items() {
return this.store.items();
}
} }
@@ -8,6 +8,7 @@
[showClear]="true" [showClear]="true"
[filter]="true" [filter]="true"
appendTo="body" appendTo="body"
(onChange)="change($event.value)"
> >
</p-select> </p-select>
</uikit-field> </uikit-field>
@@ -8,6 +8,7 @@
[showClear]="true" [showClear]="true"
[filter]="true" [filter]="true"
appendTo="body" appendTo="body"
(onChange)="change($event.value)"
> >
</p-select> </p-select>
</uikit-field> </uikit-field>
+1 -1
View File
@@ -9,7 +9,7 @@ export interface IProductRawResponse {
name: string; name: string;
description?: string; description?: string;
barcode?: Maybe<string>; barcode?: Maybe<string>;
sku?: Maybe<string>; sku: string;
// productType: string; // productType: string;
brand?: IProductBrand; brand?: IProductBrand;
category?: IProductCategory; category?: IProductCategory;
+2
View File
@@ -22,6 +22,7 @@ export class ProductsStore extends PaginatedStore<IProductResponse> {
const queryParams = activeRoute.snapshot.queryParams; const queryParams = activeRoute.snapshot.queryParams;
super({ super({
initialized: false,
loading: false, loading: false,
error: null, error: null,
isRefreshing: false, isRefreshing: false,
@@ -40,6 +41,7 @@ export class ProductsStore extends PaginatedStore<IProductResponse> {
*/ */
reset(): void { reset(): void {
this.setState({ this.setState({
initialized: false,
loading: false, loading: false,
error: null, error: null,
isRefreshing: false, isRefreshing: false,
@@ -12,10 +12,11 @@ import { getMovementReferenceTypeColor, getMovementReferenceTypeText } from './u
export class CatalogMovementReferenceTagComponent { export class CatalogMovementReferenceTagComponent {
@Input() type!: MovementReferenceType; @Input() type!: MovementReferenceType;
@Input() movementType?: MovementType; @Input() movementType?: MovementType;
@Input() counterName?: string;
constructor() {} constructor() {}
get text() { get text() {
return getMovementReferenceTypeText(this.type, this.movementType); return getMovementReferenceTypeText(this.type, this.movementType, this.counterName);
} }
get color() { get color() {
return getMovementReferenceTypeColor(this.type); return getMovementReferenceTypeColor(this.type);
@@ -5,6 +5,7 @@ import { MovementReferenceType } from './types';
export const getMovementReferenceTypeText = ( export const getMovementReferenceTypeText = (
movementType: MovementReferenceType, movementType: MovementReferenceType,
type?: MovementType, type?: MovementType,
counterName?: string,
): string => { ): string => {
switch (movementType) { switch (movementType) {
case MovementReferenceType.PURCHASE: case MovementReferenceType.PURCHASE:
@@ -15,9 +16,9 @@ export const getMovementReferenceTypeText = (
return 'تعدیل'; return 'تعدیل';
case MovementReferenceType.INVENTORY_TRANSFER: case MovementReferenceType.INVENTORY_TRANSFER:
if (type === MovementType.IN) { if (type === MovementType.IN) {
return 'دریافت از انبار دیگر'; return `دریافت از انبار ${counterName ?? 'دیگر'}`;
} else { } else {
return 'ارسال به انبار دیگر'; return `ارسال به انبار ${counterName ?? 'دیگر'}`;
} }
default: default:
return 'انتقال بین انبارها'; return 'انتقال بین انبارها';
@@ -1,5 +1,5 @@
import { Maybe } from '@/core'; import { Maybe } from '@/core';
import { Component, Input, model, signal } from '@angular/core'; import { Component, EventEmitter, Input, model, Output, signal } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
export interface ISelectItem { export interface ISelectItem {
@@ -18,6 +18,7 @@ export abstract class AbstractSelectComponent<T = ISelectItem> {
@Input() showErrors: boolean = true; @Input() showErrors: boolean = true;
@Input() isFullDataOptionValue: boolean = false; @Input() isFullDataOptionValue: boolean = false;
@Input() optionValue = 'id'; @Input() optionValue = 'id';
@Output() onChange = new EventEmitter<Maybe<T>>();
loading = signal(false); loading = signal(false);
items = signal<Maybe<T[]>>(null); items = signal<Maybe<T[]>>(null);
@@ -50,4 +51,13 @@ export abstract class AbstractSelectComponent<T = ISelectItem> {
} }
return this.optionValue; return this.optionValue;
} }
change(value: Maybe<T>) {
if (typeof value !== 'object') {
// @ts-ignore
this.onChange.emit(this.items()?.find((item) => item[this.optionValue] === value));
} else {
this.onChange.emit(value);
}
}
} }
@@ -5,22 +5,10 @@
[showErrors]="showErrors" [showErrors]="showErrors"
class="w-full datepicker" class="w-full datepicker"
> >
<div class="w-full">
<div #wrapper [attr.data-disable]="disabled" [attr.data-name]="name" class="w-full"> <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" /> <input pInputText data-input [attr.placeholder]="placeholder" [name]="name" class="w-full" />
</div> </div>
<div class="flat"></div> <div class="flat"></div>
</div>
</uikit-field> </uikit-field>
<!--
<div #wrapper class="flex items-center gap-2" data-enable-time="false" data-click-opens="true">
<input pInputText class="w-full bg-white text-sm rounded border p-2" data-input [attr.placeholder]="placeholder" />
<button pButton type="button" class="p-button-text text-gray-500" data-toggle aria-label="toggle">
<i class="pi pi-calendar"></i>
</button>
<button pButton type="button" class="p-button-text text-gray-500" data-clear aria-label="clear">
<i class="pi pi-times"></i>
</button>
</div> -->
@@ -41,11 +41,13 @@ 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'), locale: 'fa',
altInputClass: 'p-inputtext w-full', altInputClass: 'p-inputtext w-full',
dateFormat: 'Y-m-d',
altFormat: 'Y-m-d',
onChange: (selectedDates: Date[], dateStr: string) => { onChange: (selectedDates: Date[], dateStr: string) => {
this.valueChange.emit(dateStr); this.valueChange.emit(dateStr);
if (this.control) this.control.setValue(dateStr); if (this.control) this.control.setValue(dayjs(dateStr).toISOString());
}, },
}); });
} }