| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- import { Injectable, NotImplementedException } from '@nestjs/common';
- import { ConfigService } from '@nestjs/config';
- import { Prisma } from '@prisma/client';
- import { PrismaService } from '../../prisma/prisma.service';
- import {
- CUSTOMER_DEFAULT_PAGE,
- CUSTOMER_DEFAULT_PAGE_SIZE,
- CUSTOMER_MAX_PAGE_SIZE,
- } from './customers.constants';
- import { CustomerUpsertDto } from './dto/customer-upsert.dto';
- import { CustomerSummaryDto } from './dto/customer-summary.dto';
- import { ListCustomersQueryDto } from './dto/list-customers-query.dto';
- import {
- formatDateOnly,
- normalizeContactType,
- normalizeFollowStatus,
- normalizeHairLength,
- normalizeLineStatus,
- parseDateOnly,
- toFrontendHairLength,
- } from './customers.mapper';
- @Injectable()
- export class CustomersService {
- constructor(
- private readonly prisma: PrismaService,
- private readonly configService: ConfigService,
- ) {}
- async list(stylistId: bigint, query: ListCustomersQueryDto): Promise<{
- items: Array<Record<string, unknown>>;
- total: number;
- page: number;
- pageSize: number;
- }> {
- const page = query.page ?? CUSTOMER_DEFAULT_PAGE;
- const pageSize = Math.min(
- query.pageSize ?? CUSTOMER_DEFAULT_PAGE_SIZE,
- CUSTOMER_MAX_PAGE_SIZE,
- );
- const skip = (page - 1) * pageSize;
- const where = this.buildListWhere(stylistId, query);
- const items = await this.prisma.customer.findMany({
- where,
- orderBy: { createTime: 'desc' },
- skip,
- take: pageSize,
- });
- const total = await this.prisma.customer.count({ where });
- return {
- items: items.map((item) => this.toCustomerEntity(item)),
- total,
- page,
- pageSize,
- };
- }
- async getById(_id: string): Promise<never> {
- throw new NotImplementedException('Customer detail is not implemented yet');
- }
- async create(
- stylistId: bigint,
- body: CustomerUpsertDto,
- ): Promise<Record<string, unknown>> {
- const avatarUrl =
- body.image?.trim() ||
- this.configService.getOrThrow<string>('customer.defaultAvatarUrl');
- const created = await this.prisma.customer.create({
- data: {
- hairstylistId: stylistId,
- name: body.name.trim(),
- kana: body.kana?.trim() ?? '',
- phone: body.phone.trim(),
- email: body.email?.trim() ?? '',
- birthday: parseDateOnly(body.birthday),
- avatarUrl,
- contactType: normalizeContactType(body.contact),
- lineStatus: normalizeLineStatus(body.lineStatus),
- lastVisitDate: parseDateOnly(body.lastVisit),
- nextVisitDate: parseDateOnly(body.nextVisit),
- visitCycle: body.visitCycle?.trim() ?? '',
- hairLength: normalizeHairLength(body.hairLength),
- hairType: body.hairType?.trim() ?? '',
- hairCondition: body.hairCondition?.trim() ?? '',
- concern: body.concern?.trim() || null,
- desiredImage: body.desiredImage?.trim() || null,
- formulaNote: body.formulaNote?.trim() || null,
- allergyNote: body.allergyNote?.trim() || null,
- privateNote: body.privateNote?.trim() || null,
- followStatus: normalizeFollowStatus(body.followStatus),
- followTiming: body.followTiming?.trim() ?? '',
- followMessage: body.followMessage?.trim() || null,
- },
- });
- return this.toCustomerEntity(created);
- }
- async update(_id: string, _body: CustomerUpsertDto): Promise<never> {
- throw new NotImplementedException('Customer update is not implemented yet');
- }
- async getSummary(stylistId: bigint): Promise<CustomerSummaryDto> {
- const today = new Date();
- const startOfDay = new Date(
- Date.UTC(
- today.getUTCFullYear(),
- today.getUTCMonth(),
- today.getUTCDate(),
- ),
- );
- const endOfDay = new Date(
- Date.UTC(
- today.getUTCFullYear(),
- today.getUTCMonth(),
- today.getUTCDate() + 1,
- ),
- );
- const [total, todayCount, followUp, simulations] = await Promise.all([
- this.prisma.customer.count({
- where: { hairstylistId: stylistId, isDeleted: 0 },
- }),
- this.prisma.customer.count({
- where: {
- hairstylistId: stylistId,
- isDeleted: 0,
- nextVisitDate: { gte: startOfDay, lt: endOfDay },
- },
- }),
- this.prisma.customer.count({
- where: {
- hairstylistId: stylistId,
- isDeleted: 0,
- followStatus: 'NEED_FOLLOW_UP',
- },
- }),
- this.prisma.aiHairSimulation.findMany({
- where: {
- hairstylistId: stylistId,
- isDeleted: 0,
- customerId: { not: null },
- },
- select: { customerId: true },
- }),
- ]);
- return {
- total,
- today: todayCount,
- followUp,
- simulated: new Set(
- simulations
- .map((item) => item.customerId)
- .filter((value): value is bigint => value != null),
- ).size,
- };
- }
- private buildListWhere(
- stylistId: bigint,
- query: ListCustomersQueryDto,
- ): Prisma.CustomerWhereInput {
- const where: Prisma.CustomerWhereInput = {
- hairstylistId: stylistId,
- isDeleted: 0,
- };
- if (query.q?.trim()) {
- where.OR = [
- { name: { contains: query.q.trim() } },
- { phone: { contains: query.q.trim() } },
- { concern: { contains: query.q.trim() } },
- { privateNote: { contains: query.q.trim() } },
- ];
- }
- if (query.followStatus && query.followStatus !== 'ALL') {
- where.followStatus = query.followStatus;
- }
- if (query.hairLength && query.hairLength !== 'ALL') {
- where.hairLength = query.hairLength;
- }
- const now = new Date();
- const startOfToday = new Date(
- Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()),
- );
- const endOfToday = new Date(
- Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1),
- );
- const dayOfWeek = startOfToday.getUTCDay();
- const diffToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
- const startOfWeek = new Date(startOfToday);
- startOfWeek.setUTCDate(startOfWeek.getUTCDate() - diffToMonday);
- const endOfWeek = new Date(startOfWeek);
- endOfWeek.setUTCDate(endOfWeek.getUTCDate() + 7);
- const thirtyDaysAgo = new Date(startOfToday);
- thirtyDaysAgo.setUTCDate(thirtyDaysAgo.getUTCDate() - 30);
- if (query.visitStatus === 'TODAY') {
- where.nextVisitDate = { gte: startOfToday, lt: endOfToday };
- } else if (query.visitStatus === 'THIS_WEEK') {
- where.nextVisitDate = { gte: startOfWeek, lt: endOfWeek };
- } else if (query.visitStatus === 'OVER_30_DAYS_NO_VISIT') {
- where.lastVisitDate = { lt: thirtyDaysAgo };
- }
- return where;
- }
- private toCustomerEntity(customer: {
- id: bigint;
- name: string;
- kana: string;
- phone: string;
- email: string;
- birthday: Date | null;
- avatarUrl: string;
- contactType: string;
- lineStatus: string | null;
- lastVisitDate: Date | null;
- nextVisitDate: Date | null;
- visitCycle: string;
- hairLength: string | null;
- hairType: string;
- hairCondition: string;
- concern: string | null;
- desiredImage: string | null;
- formulaNote: string | null;
- allergyNote: string | null;
- privateNote: string | null;
- followStatus: string;
- followTiming: string;
- followMessage: string | null;
- }) {
- return {
- id: customer.id.toString(),
- name: customer.name,
- kana: customer.kana || undefined,
- phone: customer.phone,
- email: customer.email || undefined,
- birthday: formatDateOnly(customer.birthday),
- image: customer.avatarUrl,
- contact: customer.contactType,
- lineStatus: customer.lineStatus ?? undefined,
- lastVisit: formatDateOnly(customer.lastVisitDate),
- nextVisit: formatDateOnly(customer.nextVisitDate),
- visitCycle: customer.visitCycle || undefined,
- hairLength: toFrontendHairLength(customer.hairLength),
- hairType: customer.hairType || undefined,
- hairCondition: customer.hairCondition || undefined,
- concern: customer.concern ?? undefined,
- desiredImage: customer.desiredImage ?? undefined,
- formulaNote: customer.formulaNote ?? undefined,
- allergyNote: customer.allergyNote ?? undefined,
- privateNote: customer.privateNote ?? undefined,
- followStatus: customer.followStatus,
- followTiming: customer.followTiming || undefined,
- followMessage: customer.followMessage ?? undefined,
- };
- }
- }
|