customer search
This commit is contained in:
@@ -76,9 +76,11 @@ export class CustomersService {
|
|||||||
const salon = await this.resolveSalonForUser(user);
|
const salon = await this.resolveSalonForUser(user);
|
||||||
const summaries = await this.buildSalonCustomerSummaries(salon.id);
|
const summaries = await this.buildSalonCustomerSummaries(salon.id);
|
||||||
const filtered = this.filterSalonCustomers(summaries, query);
|
const filtered = this.filterSalonCustomers(summaries, query);
|
||||||
|
const sorted = this.sortSalonCustomersByRecent(filtered);
|
||||||
|
const customers = query.limit ? sorted.slice(0, query.limit) : sorted;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
customers: filtered,
|
customers,
|
||||||
stats: this.buildSalonCustomerStats(summaries),
|
stats: this.buildSalonCustomerStats(summaries),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -335,15 +337,18 @@ export class CustomersService {
|
|||||||
customers: SalonCustomerSummaryDto[],
|
customers: SalonCustomerSummaryDto[],
|
||||||
query: FindSalonCustomersQueryDto,
|
query: FindSalonCustomersQueryDto,
|
||||||
): SalonCustomerSummaryDto[] {
|
): SalonCustomerSummaryDto[] {
|
||||||
const search = query.search?.trim().toLowerCase();
|
const search = query.search
|
||||||
|
? this.normalizeSearchText(query.search)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return customers.filter((customer) => {
|
return customers.filter((customer) => {
|
||||||
const fullName =
|
const fullName =
|
||||||
`${customer.firstName} ${customer.lastName}`.toLowerCase();
|
`${customer.firstName} ${customer.lastName}`.toLowerCase();
|
||||||
|
const normalizedMobile = this.normalizeSearchText(customer.mobile);
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
!search ||
|
!search ||
|
||||||
fullName.includes(search) ||
|
fullName.includes(search) ||
|
||||||
customer.mobile.includes(search);
|
normalizedMobile.includes(search);
|
||||||
const matchesStatus =
|
const matchesStatus =
|
||||||
!query.status || customer.status === query.status;
|
!query.status || customer.status === query.status;
|
||||||
|
|
||||||
@@ -351,6 +356,24 @@ export class CustomersService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sortSalonCustomersByRecent(
|
||||||
|
customers: SalonCustomerSummaryDto[],
|
||||||
|
): SalonCustomerSummaryDto[] {
|
||||||
|
return [...customers].sort((a, b) => {
|
||||||
|
const dateA = new Date(a.lastVisitDate ?? a.membershipDate).getTime();
|
||||||
|
const dateB = new Date(b.lastVisitDate ?? b.membershipDate).getTime();
|
||||||
|
return dateB - dateA;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeSearchText(value: string): string {
|
||||||
|
return value
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[۰-۹]/g, (digit) => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(digit)))
|
||||||
|
.replace(/[٠-٩]/g, (digit) => String('٠١٢٣٤٥٦٧٨٩'.indexOf(digit)));
|
||||||
|
}
|
||||||
|
|
||||||
private buildSalonCustomerStats(customers: SalonCustomerSummaryDto[]) {
|
private buildSalonCustomerStats(customers: SalonCustomerSummaryDto[]) {
|
||||||
return {
|
return {
|
||||||
total: customers.length,
|
total: customers.length,
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
IsDateString,
|
IsDateString,
|
||||||
IsIn,
|
IsIn,
|
||||||
|
IsInt,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
Length,
|
Length,
|
||||||
Matches,
|
Matches,
|
||||||
|
Max,
|
||||||
|
Min,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
export class CreateSalonCustomerDto {
|
export class CreateSalonCustomerDto {
|
||||||
@@ -50,4 +54,15 @@ export class FindSalonCustomersQueryDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsIn(['active', 'at-risk', 'lost', 'vip'])
|
@IsIn(['active', 'at-risk', 'lost', 'vip'])
|
||||||
status?: 'active' | 'at-risk' | 'lost' | 'vip';
|
status?: 'active' | 'at-risk' | 'lost' | 'vip';
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Maximum number of customers to return (most recent first)',
|
||||||
|
example: 10,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Max(50)
|
||||||
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user