Files
v3-api/src/expert-prepared-messages/expert-prepared-messages.service.ts

265 lines
7.9 KiB
TypeScript

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import type { FilterQuery } from 'mongoose';
import {
BaseResponseDTO,
PageMetaDto,
} from 'src/common/dto/base-response.dto';
import {
EXPERT_PREPARED_MESSAGE_CATEGORIES,
EXPERT_PREPARED_MESSAGE_CATEGORY_LABELS,
ExpertPreparedMessageCategory,
} from 'src/common/types/expert-prepared-message-category.type';
import { ExpertPreparedMessageModel } from 'src/database/model/expert-prepared-message.model';
import { CreateExpertPreparedMessageDto } from './dto/create-expert-prepared-message.dto';
import { ListExpertPreparedMessagesQueryDto } from './dto/list-expert-prepared-messages-query.dto';
import { ReorderExpertPreparedMessagesDto } from './dto/reorder-expert-prepared-messages.dto';
import { UpdateExpertPreparedMessageDto } from './dto/update-expert-prepared-message.dto';
export type ExpertPreparedMessageItem = {
id: string;
message: string;
label: string;
category: ExpertPreparedMessageCategory;
sortOrder: number;
locale: string;
};
export type ExpertPreparedMessageCategorySummary = {
value: ExpertPreparedMessageCategory;
label: string;
messageCount: number;
};
@Injectable()
export class ExpertPreparedMessagesService {
constructor(
@InjectModel(ExpertPreparedMessageModel.name)
private readonly preparedMessageModel: Model<ExpertPreparedMessageModel>,
) {}
getCategories() {
return EXPERT_PREPARED_MESSAGE_CATEGORIES.map((value) => ({
value,
label: EXPERT_PREPARED_MESSAGE_CATEGORY_LABELS[value],
}));
}
async findAllForAdmin(query: ListExpertPreparedMessagesQueryDto) {
const filter: FilterQuery<ExpertPreparedMessageModel> = {};
if (query.category) filter.category = query.category;
if (query.isEnabled !== undefined) filter.isEnabled = query.isEnabled;
if (query.locale) filter.locale = query.locale;
const [items, itemCount] = await Promise.all([
this.preparedMessageModel
.find(filter)
.sort({ category: 1, sortOrder: 1, createdAt: 1 })
.skip(query.skip)
.limit(query.take)
.lean()
.exec(),
this.preparedMessageModel.countDocuments(filter).exec(),
]);
const meta = new PageMetaDto({ pageOptionsDto: query, itemCount });
return new BaseResponseDTO(
HttpStatus.OK,
'Expert prepared messages retrieved successfully',
items.map((doc) => this.toAdminDto(doc)),
meta,
);
}
async findEnabledCategoriesForExpert(locale = 'fa') {
const counts = await this.preparedMessageModel
.aggregate<{ _id: ExpertPreparedMessageCategory; count: number }>([
{ $match: { isEnabled: true, locale } },
{ $group: { _id: '$category', count: { $sum: 1 } } },
])
.exec();
const countByCategory = new Map(
counts.map((row) => [row._id, row.count]),
);
const categories: ExpertPreparedMessageCategorySummary[] =
EXPERT_PREPARED_MESSAGE_CATEGORIES.filter((value) =>
countByCategory.has(value),
).map((value) => ({
value,
label: EXPERT_PREPARED_MESSAGE_CATEGORY_LABELS[value],
messageCount: countByCategory.get(value) ?? 0,
}));
return new BaseResponseDTO(HttpStatus.OK, 'SUCCESS', {
locale,
categories,
});
}
async findEnabledByCategoryForExpert(
category: ExpertPreparedMessageCategory,
locale = 'fa',
) {
const docs = await this.preparedMessageModel
.find({ isEnabled: true, locale, category })
.sort({ sortOrder: 1, createdAt: 1 })
.lean()
.exec();
return new BaseResponseDTO(HttpStatus.OK, 'SUCCESS', {
locale,
category,
label: EXPERT_PREPARED_MESSAGE_CATEGORY_LABELS[category],
messages: docs.map((doc) => this.toExpertItem(doc)),
});
}
async create(dto: CreateExpertPreparedMessageDto, adminUsername: string) {
const sortOrder =
dto.sortOrder ??
(await this.nextSortOrderForCategory(dto.category, dto.locale ?? 'fa'));
const created = await this.preparedMessageModel.create({
message: dto.message.trim(),
category: dto.category,
label: (dto.label ?? '').trim(),
isEnabled: dto.isEnabled ?? true,
sortOrder,
locale: (dto.locale ?? 'fa').trim(),
createdBy: adminUsername,
updatedBy: adminUsername,
});
return new BaseResponseDTO(
HttpStatus.CREATED,
'Expert prepared message created successfully',
this.toAdminDto(created.toObject()),
);
}
async update(
id: string,
dto: UpdateExpertPreparedMessageDto,
adminUsername: string,
) {
const existing = await this.preparedMessageModel.findById(id).exec();
if (!existing) {
throw new HttpException(
'prepared_message_not_found',
HttpStatus.NOT_FOUND,
);
}
if (dto.message !== undefined) existing.message = dto.message.trim();
if (dto.category !== undefined) existing.category = dto.category;
if (dto.label !== undefined) existing.label = dto.label.trim();
if (dto.isEnabled !== undefined) existing.isEnabled = dto.isEnabled;
if (dto.sortOrder !== undefined) existing.sortOrder = dto.sortOrder;
if (dto.locale !== undefined) existing.locale = dto.locale.trim();
existing.updatedBy = adminUsername;
const saved = await existing.save();
return new BaseResponseDTO(
HttpStatus.OK,
'Expert prepared message updated successfully',
this.toAdminDto(saved.toObject()),
);
}
async toggleEnabled(id: string, adminUsername: string) {
const existing = await this.preparedMessageModel.findById(id).exec();
if (!existing) {
throw new HttpException(
'prepared_message_not_found',
HttpStatus.NOT_FOUND,
);
}
existing.isEnabled = !existing.isEnabled;
existing.updatedBy = adminUsername;
const saved = await existing.save();
return new BaseResponseDTO(
HttpStatus.OK,
'Expert prepared message toggled successfully',
this.toAdminDto(saved.toObject()),
);
}
async reorder(dto: ReorderExpertPreparedMessagesDto, adminUsername: string) {
const bulk = dto.items.map((item) => ({
updateOne: {
filter: { _id: item.id },
update: { $set: { sortOrder: item.sortOrder, updatedBy: adminUsername } },
},
}));
await this.preparedMessageModel.bulkWrite(bulk);
return new BaseResponseDTO(
HttpStatus.OK,
'Expert prepared messages reordered successfully',
null,
);
}
async remove(id: string) {
const result = await this.preparedMessageModel.findByIdAndDelete(id).exec();
if (!result) {
throw new HttpException(
'prepared_message_not_found',
HttpStatus.NOT_FOUND,
);
}
return new BaseResponseDTO(
HttpStatus.OK,
'Expert prepared message deleted successfully',
null,
);
}
private async nextSortOrderForCategory(
category: ExpertPreparedMessageCategory,
locale: string,
): Promise<number> {
const last = await this.preparedMessageModel
.findOne({ category, locale })
.sort({ sortOrder: -1 })
.select({ sortOrder: 1 })
.lean()
.exec();
return (last?.sortOrder ?? -1) + 1;
}
private toExpertItem(doc: any): ExpertPreparedMessageItem {
return {
id: String(doc._id),
message: doc.message,
label: doc.label || doc.message.slice(0, 40),
category: doc.category,
sortOrder: doc.sortOrder ?? 0,
locale: doc.locale ?? 'fa',
};
}
private toAdminDto(doc: any) {
return {
id: String(doc._id),
message: doc.message,
label: doc.label ?? '',
category: doc.category,
isEnabled: doc.isEnabled,
sortOrder: doc.sortOrder ?? 0,
locale: doc.locale ?? 'fa',
createdBy: doc.createdBy ?? null,
updatedBy: doc.updatedBy ?? null,
createdAt: doc.createdAt,
updatedAt: doc.updatedAt,
};
}
}