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, ) {} getCategories() { return EXPERT_PREPARED_MESSAGE_CATEGORIES.map((value) => ({ value, label: EXPERT_PREPARED_MESSAGE_CATEGORY_LABELS[value], })); } async findAllForAdmin(query: ListExpertPreparedMessagesQueryDto) { const filter: FilterQuery = {}; 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 { 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, }; } }