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()">
|
<form [formGroup]="form" (submit)="submit()">
|
||||||
<p-message severity="info"> وارد کردن شماره اقتصادی و یا کد ملی به همراه کد پستی الزامی است. </p-message>
|
<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-first-name [control]="form.controls.first_name" />
|
||||||
<field-last-name [control]="form.controls.last_name" />
|
<field-last-name [control]="form.controls.last_name" />
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { Message } from 'primeng/message';
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { IIndividualCustomer } from '../../../models/customer';
|
import { IIndividualCustomer } from '../../../models/customer';
|
||||||
import { PosLandingStore } from '../../../store/main.store';
|
import { PosLandingStore } from '../../../store/main.store';
|
||||||
|
import { CustomerSelectFieldComponent } from '../customerSelectField/customer-select-field.component';
|
||||||
|
import { IIndividualCustomerResponse } from '../customerSelectField/models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pos-customer-individual-form',
|
selector: 'pos-customer-individual-form',
|
||||||
@@ -28,6 +30,7 @@ import { PosLandingStore } from '../../../store/main.store';
|
|||||||
PostalCodeComponent,
|
PostalCodeComponent,
|
||||||
Message,
|
Message,
|
||||||
NationalIdComponent,
|
NationalIdComponent,
|
||||||
|
CustomerSelectFieldComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CustomerIndividualFormComponent extends AbstractForm<
|
export class CustomerIndividualFormComponent extends AbstractForm<
|
||||||
@@ -35,6 +38,7 @@ export class CustomerIndividualFormComponent extends AbstractForm<
|
|||||||
IIndividualCustomer
|
IIndividualCustomer
|
||||||
> {
|
> {
|
||||||
private readonly store = inject(PosLandingStore);
|
private readonly store = inject(PosLandingStore);
|
||||||
|
private selectedCustomerId?: string;
|
||||||
costumerInfo = computed(() => this.store.customer().info?.customer_individual);
|
costumerInfo = computed(() => this.store.customer().info?.customer_individual);
|
||||||
|
|
||||||
override showSuccessMessage = false;
|
override showSuccessMessage = false;
|
||||||
@@ -93,7 +97,7 @@ export class CustomerIndividualFormComponent extends AbstractForm<
|
|||||||
return new Observable<IIndividualCustomer>((observer) => {
|
return new Observable<IIndividualCustomer>((observer) => {
|
||||||
const info = this.form.value as IIndividualCustomer;
|
const info = this.form.value as IIndividualCustomer;
|
||||||
this.store.setCustomer({
|
this.store.setCustomer({
|
||||||
customer_id: '',
|
customer_id: this.selectedCustomerId,
|
||||||
type: CustomerType.INDIVIDUAL,
|
type: CustomerType.INDIVIDUAL,
|
||||||
info: {
|
info: {
|
||||||
customer_individual: {
|
customer_individual: {
|
||||||
@@ -105,4 +109,18 @@ export class CustomerIndividualFormComponent extends AbstractForm<
|
|||||||
return observer.next(info);
|
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 severity="info" icon="pi pi-info-circle">
|
||||||
وارد کردن شماره اقتصادی و یا شناسه ملی به همراه کد پستی الزامی است.
|
وارد کردن شماره اقتصادی و یا شناسه ملی به همراه کد پستی الزامی است.
|
||||||
</p-message>
|
</p-message>
|
||||||
|
<pos-customer-select-field
|
||||||
|
type="LEGAL"
|
||||||
|
[debounceTime]="400"
|
||||||
|
(selected)="onCustomerSelected($event)" />
|
||||||
<field-name [control]="form.controls.name" name="name" label="نام مجموعه" />
|
<field-name [control]="form.controls.name" name="name" label="نام مجموعه" />
|
||||||
<field-legal-economic-code [control]="form.controls.economic_code" />
|
<field-legal-economic-code [control]="form.controls.economic_code" />
|
||||||
<field-registration-number [control]="form.controls.registration_number" />
|
<field-registration-number [control]="form.controls.registration_number" />
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { Message } from 'primeng/message';
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { ILegalCustomer } from '../../../models/customer';
|
import { ILegalCustomer } from '../../../models/customer';
|
||||||
import { PosLandingStore } from '../../../store/main.store';
|
import { PosLandingStore } from '../../../store/main.store';
|
||||||
|
import { CustomerSelectFieldComponent } from '../customerSelectField/customer-select-field.component';
|
||||||
|
import { ILegalCustomerResponse } from '../customerSelectField/models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pos-customer-legal-form',
|
selector: 'pos-customer-legal-form',
|
||||||
@@ -26,10 +28,12 @@ import { PosLandingStore } from '../../../store/main.store';
|
|||||||
RegistrationNumberComponent,
|
RegistrationNumberComponent,
|
||||||
PostalCodeComponent,
|
PostalCodeComponent,
|
||||||
Message,
|
Message,
|
||||||
|
CustomerSelectFieldComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CustomerLegalFormComponent extends AbstractForm<ILegalCustomer, ILegalCustomer> {
|
export class CustomerLegalFormComponent extends AbstractForm<ILegalCustomer, ILegalCustomer> {
|
||||||
private readonly store = inject(PosLandingStore);
|
private readonly store = inject(PosLandingStore);
|
||||||
|
private selectedCustomerId?: string;
|
||||||
|
|
||||||
override showSuccessMessage = false;
|
override showSuccessMessage = false;
|
||||||
|
|
||||||
@@ -93,7 +97,7 @@ export class CustomerLegalFormComponent extends AbstractForm<ILegalCustomer, ILe
|
|||||||
return new Observable<ILegalCustomer>((observer) => {
|
return new Observable<ILegalCustomer>((observer) => {
|
||||||
const info = this.form.value as ILegalCustomer;
|
const info = this.form.value as ILegalCustomer;
|
||||||
this.store.setCustomer({
|
this.store.setCustomer({
|
||||||
customer_id: '',
|
customer_id: this.selectedCustomerId,
|
||||||
type: CustomerType.LEGAL,
|
type: CustomerType.LEGAL,
|
||||||
info: {
|
info: {
|
||||||
customer_legal: {
|
customer_legal: {
|
||||||
@@ -105,4 +109,17 @@ export class CustomerLegalFormComponent extends AbstractForm<ILegalCustomer, ILe
|
|||||||
return observer.next(info);
|
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 = {
|
export const environment = {
|
||||||
production: true,
|
production: true,
|
||||||
// apiBaseUrl: 'https://psp-api.shift-am.ir',
|
// apiBaseUrl: 'https://psp-api.shift-am.ir',
|
||||||
apiBaseUrl: 'http://192.168.128.73:5002',
|
// apiBaseUrl: 'http://192.168.128.73:5002',
|
||||||
// apiBaseUrl: 'http://localhost:5002',
|
apiBaseUrl: 'http://localhost:5002',
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 5000,
|
port: 5000,
|
||||||
enableLogging: false,
|
enableLogging: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user