feat: add customer selection component for individual and legal customers, update forms to utilize new component
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
||||
<p-autocomplete
|
||||
(completeMethod)="search($event)"
|
||||
(onSelect)="onSelectOption($event)"
|
||||
[suggestions]="suggestions() || []"
|
||||
optionLabel="label"
|
||||
[optionValue]="undefined"
|
||||
[dropdown]="true"
|
||||
[forceSelection]="false"
|
||||
fluid
|
||||
[minLength]="1"
|
||||
placeholder="نام، شماره اقتصادی، شناسه ملی، کد پستی ...">
|
||||
<ng-template let-option #item>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="font-semibold">{{ option.main || '-----' }}</span>
|
||||
<span class="text-muted-color text-sm">{{ option.sub }}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
</p-autocomplete>
|
||||
+99
@@ -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<string>;
|
||||
@Input() type: CustomerSearchType = 'LEGAL';
|
||||
@Input() debounceTime = 300;
|
||||
@Output() selected = new EventEmitter<any>();
|
||||
|
||||
constructor(private readonly customersService: PosSaleInvoicesService) {}
|
||||
|
||||
suggestions = signal<Maybe<{ id: string; label: string; main: string; sub: string; item: any }[]>>(
|
||||
null
|
||||
);
|
||||
private readonly search$ = new Subject<string>();
|
||||
private readonly destroy$ = new Subject<void>();
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
+37
@@ -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;
|
||||
}
|
||||
+63
@@ -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<IListingResponse<IIndividualCustomerResponse>> {
|
||||
return this.http
|
||||
.get<IListingResponse<IIndividualCustomerRawResponse>>(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<IListingResponse<ILegalCustomerResponse>> {
|
||||
return this.http
|
||||
.get<IListingResponse<ILegalCustomerRawResponse>>(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<IListingResponse<IIndividualCustomerResponse | ILegalCustomerResponse>> {
|
||||
return type === 'INDIVIDUAL' ? this.getIndividual(q) : this.getLegal(q);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
<form [formGroup]="form" (submit)="submit()">
|
||||
<p-message severity="info"> وارد کردن شماره اقتصادی و یا کد ملی به همراه کد پستی الزامی است. </p-message>
|
||||
<pos-customer-select-field
|
||||
type="INDIVIDUAL"
|
||||
[debounceTime]="400"
|
||||
(selected)="onCustomerSelected($event)" />
|
||||
|
||||
<field-first-name [control]="form.controls.first_name" />
|
||||
<field-last-name [control]="form.controls.last_name" />
|
||||
|
||||
@@ -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<IIndividualCustomer>((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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
<p-message severity="info" icon="pi pi-info-circle">
|
||||
وارد کردن شماره اقتصادی و یا شناسه ملی به همراه کد پستی الزامی است.
|
||||
</p-message>
|
||||
<pos-customer-select-field
|
||||
type="LEGAL"
|
||||
[debounceTime]="400"
|
||||
(selected)="onCustomerSelected($event)" />
|
||||
<field-name [control]="form.controls.name" name="name" label="نام مجموعه" />
|
||||
<field-legal-economic-code [control]="form.controls.economic_code" />
|
||||
<field-registration-number [control]="form.controls.registration_number" />
|
||||
|
||||
@@ -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<ILegalCustomer, ILegalCustomer> {
|
||||
private readonly store = inject(PosLandingStore);
|
||||
private selectedCustomerId?: string;
|
||||
|
||||
override showSuccessMessage = false;
|
||||
|
||||
@@ -93,7 +97,7 @@ export class CustomerLegalFormComponent extends AbstractForm<ILegalCustomer, ILe
|
||||
return new Observable<ILegalCustomer>((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<ILegalCustomer, ILe
|
||||
return observer.next(info);
|
||||
});
|
||||
}
|
||||
|
||||
onCustomerSelected(customer: ILegalCustomerResponse) {
|
||||
this.selectedCustomerId = customer?.customer_id;
|
||||
this.form.patchValue(
|
||||
{
|
||||
name: customer?.name ?? '',
|
||||
economic_code: customer?.economic_code ?? '',
|
||||
registration_number: customer?.registration_number ?? '',
|
||||
postal_code: customer?.postal_code ?? '',
|
||||
},
|
||||
{ emitEvent: false }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
// apiBaseUrl: 'https://psp-api.shift-am.ir',
|
||||
apiBaseUrl: 'http://192.168.128.73:5002',
|
||||
// apiBaseUrl: 'http://localhost:5002',
|
||||
// apiBaseUrl: 'http://192.168.128.73:5002',
|
||||
apiBaseUrl: 'http://localhost:5002',
|
||||
host: 'localhost',
|
||||
port: 5000,
|
||||
enableLogging: false,
|
||||
|
||||
Reference in New Issue
Block a user