From 560b3516e1a04f37f253ed84867d3c9a10e50286 Mon Sep 17 00:00:00 2001 From: ahasani194 Date: Fri, 5 Jun 2026 01:33:37 +0330 Subject: [PATCH] feat: add customer selection component for individual and legal customers, update forms to utilize new component --- .../customer-select-field.component.html | 18 ++++ .../customer-select-field.component.ts | 99 +++++++++++++++++++ .../customerSelectField/models/index.ts | 37 +++++++ .../customerSelectField/services/index.ts | 63 ++++++++++++ .../customers/individual/form.component.html | 4 + .../customers/individual/form.component.ts | 20 +++- .../customers/legal/form.component.html | 4 + .../customers/legal/form.component.ts | 19 +++- src/environments/environment.tis.ts | 4 +- 9 files changed, 264 insertions(+), 4 deletions(-) create mode 100644 src/app/domains/pos/modules/shop/components/customers/customerSelectField/customer-select-field.component.html create mode 100644 src/app/domains/pos/modules/shop/components/customers/customerSelectField/customer-select-field.component.ts create mode 100644 src/app/domains/pos/modules/shop/components/customers/customerSelectField/models/index.ts create mode 100644 src/app/domains/pos/modules/shop/components/customers/customerSelectField/services/index.ts diff --git a/src/app/domains/pos/modules/shop/components/customers/customerSelectField/customer-select-field.component.html b/src/app/domains/pos/modules/shop/components/customers/customerSelectField/customer-select-field.component.html new file mode 100644 index 0000000..3b309c6 --- /dev/null +++ b/src/app/domains/pos/modules/shop/components/customers/customerSelectField/customer-select-field.component.html @@ -0,0 +1,18 @@ + + +
+ {{ option.main || '-----' }} + {{ option.sub }} +
+
+
diff --git a/src/app/domains/pos/modules/shop/components/customers/customerSelectField/customer-select-field.component.ts b/src/app/domains/pos/modules/shop/components/customers/customerSelectField/customer-select-field.component.ts new file mode 100644 index 0000000..973df26 --- /dev/null +++ b/src/app/domains/pos/modules/shop/components/customers/customerSelectField/customer-select-field.component.ts @@ -0,0 +1,99 @@ +import { Maybe } from '@/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, signal } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { Subject, of } from 'rxjs'; +import { catchError, debounceTime, distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators'; +import { AutoCompleteModule } from 'primeng/autocomplete'; +import { CustomerSearchType, PosSaleInvoicesService } from './services'; + +@Component({ + selector: 'pos-customer-select-field', + templateUrl: `customer-select-field.component.html`, + imports: [AutoCompleteModule, ReactiveFormsModule], +}) +export class CustomerSelectFieldComponent implements OnInit, OnDestroy { + // @Input() control!: FormControl; + @Input() type: CustomerSearchType = 'LEGAL'; + @Input() debounceTime = 300; + @Output() selected = new EventEmitter(); + + constructor(private readonly customersService: PosSaleInvoicesService) {} + + suggestions = signal>( + null + ); + private readonly search$ = new Subject(); + private readonly destroy$ = new Subject(); + + ngOnInit() { + this.search$ + .pipe( + debounceTime(this.debounceTime), + distinctUntilChanged(), + switchMap((query) => { + if (!query?.trim()) { + return of([]); + } + + const request$ = this.customersService.getByType(this.type, query); + + return request$.pipe( + switchMap((res) => of(res.data ?? [])), + catchError(() => of([])) + ); + }), + takeUntil(this.destroy$) + ) + .subscribe((items: any[]) => { + this.suggestions.set(items.map((item) => this.mapOption(item))); + }); + } + + search(event: { query: string }) { + this.search$.next(event.query ?? ''); + } + + onSelectOption(event: { value?: { item?: any } }) { + const item = event?.value?.item; + if (item) { + this.selected.emit(item); + } + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + private mapOption(item: any): { id: string; label: string; main: string; sub: string; item: any } { + if (this.type === 'INDIVIDUAL') { + const firstName = String(item?.first_name ?? '').trim(); + const lastName = String(item?.last_name ?? '').trim(); + const fullName = `${firstName} ${lastName}`.trim(); + const nationalId = String(item?.national_id ?? '').trim(); + const mobile = String(item?.mobile_number ?? '').trim(); + const sub = [nationalId, mobile].filter(Boolean).join(' | '); + + return { + id: nationalId || mobile || fullName, + label: [fullName, nationalId, mobile].filter(Boolean).join(' - '), + main: fullName, + sub, + item, + }; + } + + const name = String(item?.name ?? '').trim(); + const registrationNumber = String(item?.registration_number ?? '').trim(); + const economicCode = String(item?.economic_code ?? '').trim(); + const sub = [registrationNumber, economicCode].filter(Boolean).join(' | '); + + return { + id: economicCode || registrationNumber || name, + label: [name, registrationNumber, economicCode].filter(Boolean).join(' - '), + main: name, + sub, + item, + }; + } +} diff --git a/src/app/domains/pos/modules/shop/components/customers/customerSelectField/models/index.ts b/src/app/domains/pos/modules/shop/components/customers/customerSelectField/models/index.ts new file mode 100644 index 0000000..5107855 --- /dev/null +++ b/src/app/domains/pos/modules/shop/components/customers/customerSelectField/models/index.ts @@ -0,0 +1,37 @@ +export interface ILegalCustomerRawResponse { + id: string; + legal: { + name?: string; + economic_code?: string; + postal_code?: string; + registration_number?: string; + }; +} +export interface ILegalCustomerResponse { + customer_id: string; + name?: string; + economic_code?: string; + postal_code?: string; + registration_number?: string; +} + +export interface IIndividualCustomerRawResponse { + id: string; + individual: { + first_name?: string; + last_name?: string; + economic_code?: string; + postal_code?: string; + mobile_number?: string; + national_id?: string; + }; +} +export interface IIndividualCustomerResponse { + customer_id?: string; + first_name?: string; + last_name?: string; + economic_code?: string; + postal_code?: string; + mobile_number?: string; + national_id?: string; +} diff --git a/src/app/domains/pos/modules/shop/components/customers/customerSelectField/services/index.ts b/src/app/domains/pos/modules/shop/components/customers/customerSelectField/services/index.ts new file mode 100644 index 0000000..88a28a6 --- /dev/null +++ b/src/app/domains/pos/modules/shop/components/customers/customerSelectField/services/index.ts @@ -0,0 +1,63 @@ +import { IListingResponse } from '@/core/models/service.model'; +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { map, Observable } from 'rxjs'; +import { + IIndividualCustomerRawResponse, + IIndividualCustomerResponse, + ILegalCustomerRawResponse, + ILegalCustomerResponse, +} from '../models'; + +export type CustomerSearchType = 'LEGAL' | 'INDIVIDUAL'; + +@Injectable({ providedIn: 'root' }) +export class PosSaleInvoicesService { + constructor(private http: HttpClient) {} + + private apiRoutes = `/api/v1/pos/customers`; + + getIndividual(q: string): Observable> { + return this.http + .get>(this.apiRoutes, { + params: { + type: 'INDIVIDUAL', + q, + }, + }) + .pipe( + map((res) => ({ + ...res, + data: res.data.map((item) => ({ + customer_id: item.id, + ...item.individual, + })), + })) + ); + } + getLegal(q: string): Observable> { + return this.http + .get>(this.apiRoutes, { + params: { + type: 'LEGAL', + q, + }, + }) + .pipe( + map((res) => ({ + ...res, + data: res.data.map((item) => ({ + customer_id: item.id, + ...item.legal, + })), + })) + ); + } + + getByType( + type: CustomerSearchType, + q: string + ): Observable> { + return type === 'INDIVIDUAL' ? this.getIndividual(q) : this.getLegal(q); + } +} diff --git a/src/app/domains/pos/modules/shop/components/customers/individual/form.component.html b/src/app/domains/pos/modules/shop/components/customers/individual/form.component.html index 5abc5e6..f26e7f9 100644 --- a/src/app/domains/pos/modules/shop/components/customers/individual/form.component.html +++ b/src/app/domains/pos/modules/shop/components/customers/individual/form.component.html @@ -1,5 +1,9 @@
وارد کردن شماره اقتصادی و یا کد ملی به همراه کد پستی الزامی است. + diff --git a/src/app/domains/pos/modules/shop/components/customers/individual/form.component.ts b/src/app/domains/pos/modules/shop/components/customers/individual/form.component.ts index 95a79fd..cc79df8 100644 --- a/src/app/domains/pos/modules/shop/components/customers/individual/form.component.ts +++ b/src/app/domains/pos/modules/shop/components/customers/individual/form.component.ts @@ -15,6 +15,8 @@ import { Message } from 'primeng/message'; import { Observable } from 'rxjs'; import { IIndividualCustomer } from '../../../models/customer'; import { PosLandingStore } from '../../../store/main.store'; +import { CustomerSelectFieldComponent } from '../customerSelectField/customer-select-field.component'; +import { IIndividualCustomerResponse } from '../customerSelectField/models'; @Component({ selector: 'pos-customer-individual-form', @@ -28,6 +30,7 @@ import { PosLandingStore } from '../../../store/main.store'; PostalCodeComponent, Message, NationalIdComponent, + CustomerSelectFieldComponent, ], }) export class CustomerIndividualFormComponent extends AbstractForm< @@ -35,6 +38,7 @@ export class CustomerIndividualFormComponent extends AbstractForm< IIndividualCustomer > { private readonly store = inject(PosLandingStore); + private selectedCustomerId?: string; costumerInfo = computed(() => this.store.customer().info?.customer_individual); override showSuccessMessage = false; @@ -93,7 +97,7 @@ export class CustomerIndividualFormComponent extends AbstractForm< return new Observable((observer) => { const info = this.form.value as IIndividualCustomer; this.store.setCustomer({ - customer_id: '', + customer_id: this.selectedCustomerId, type: CustomerType.INDIVIDUAL, info: { customer_individual: { @@ -105,4 +109,18 @@ export class CustomerIndividualFormComponent extends AbstractForm< return observer.next(info); }); } + + onCustomerSelected(customer: IIndividualCustomerResponse) { + this.selectedCustomerId = customer?.customer_id; + this.form.patchValue( + { + first_name: customer?.first_name ?? '', + last_name: customer?.last_name ?? '', + economic_code: customer?.economic_code ?? '', + national_id: customer?.national_id ?? '', + postal_code: customer?.postal_code ?? '', + }, + { emitEvent: false } + ); + } } diff --git a/src/app/domains/pos/modules/shop/components/customers/legal/form.component.html b/src/app/domains/pos/modules/shop/components/customers/legal/form.component.html index aa2cf96..dfbc24c 100644 --- a/src/app/domains/pos/modules/shop/components/customers/legal/form.component.html +++ b/src/app/domains/pos/modules/shop/components/customers/legal/form.component.html @@ -2,6 +2,10 @@ وارد کردن شماره اقتصادی و یا شناسه‌ ملی به همراه کد پستی الزامی است. + diff --git a/src/app/domains/pos/modules/shop/components/customers/legal/form.component.ts b/src/app/domains/pos/modules/shop/components/customers/legal/form.component.ts index 90e3b13..c324f63 100644 --- a/src/app/domains/pos/modules/shop/components/customers/legal/form.component.ts +++ b/src/app/domains/pos/modules/shop/components/customers/legal/form.component.ts @@ -14,6 +14,8 @@ import { Message } from 'primeng/message'; import { Observable } from 'rxjs'; import { ILegalCustomer } from '../../../models/customer'; import { PosLandingStore } from '../../../store/main.store'; +import { CustomerSelectFieldComponent } from '../customerSelectField/customer-select-field.component'; +import { ILegalCustomerResponse } from '../customerSelectField/models'; @Component({ selector: 'pos-customer-legal-form', @@ -26,10 +28,12 @@ import { PosLandingStore } from '../../../store/main.store'; RegistrationNumberComponent, PostalCodeComponent, Message, + CustomerSelectFieldComponent, ], }) export class CustomerLegalFormComponent extends AbstractForm { private readonly store = inject(PosLandingStore); + private selectedCustomerId?: string; override showSuccessMessage = false; @@ -93,7 +97,7 @@ export class CustomerLegalFormComponent extends AbstractForm((observer) => { const info = this.form.value as ILegalCustomer; this.store.setCustomer({ - customer_id: '', + customer_id: this.selectedCustomerId, type: CustomerType.LEGAL, info: { customer_legal: { @@ -105,4 +109,17 @@ export class CustomerLegalFormComponent extends AbstractForm