Chat settings in Super Admin and changing the chat title and position.

This commit is contained in:
2026-09-29 14:45:19 +03:30
parent 125976f3eb
commit 66af7e577c
14 changed files with 332 additions and 269 deletions
@@ -1,4 +1,4 @@
import { Body, Controller, Get, Patch, Post, UseGuards } from '@nestjs/common';
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';
import { RequireOrgModule } from '../billing/org-module.decorator';
@@ -6,7 +6,6 @@ import { OrgModuleGuard } from '../billing/org-module.guard';
import {
CreateConversationDmDto,
CreateConversationGroupDto,
UpdateConversationSettingsDto,
} from './dto';
import { ConversationsService } from './conversations.service';
@@ -21,19 +20,6 @@ export class ConversationsController {
return this.conversations.getAccess(user.userId, user.organizationId, user.role);
}
@Get('settings')
settings(@CurrentUser() user: { organizationId: string; role: string }) {
return this.conversations.getSettings(user.organizationId);
}
@Patch('settings')
updateSettings(
@CurrentUser() user: { organizationId: string; role: string },
@Body() dto: UpdateConversationSettingsDto,
) {
return this.conversations.updateSettings(user.organizationId, user.role, dto);
}
@Get('session')
session(@CurrentUser() user: { userId: string; organizationId: string; role: string }) {
return this.conversations.getSession(user.userId, user.organizationId, user.role);
@@ -12,9 +12,7 @@ import type {
ConversationSettingsDto,
PlatformConversationStatsDto,
UpdateConversationSettingsRequest,
UserRole,
} from '@mento/shared';
import { canManageSettings } from '@mento/shared';
import { PrismaService } from '../prisma/prisma.service';
import { TahesabCryptoService } from '../integrations/tahesab/tahesab-crypto.service';
import { ChatExternalClient, type ExternalRoomRow } from './chat-external.client';
@@ -59,14 +57,17 @@ export class ConversationsService {
};
}
async updateSettings(
/** ذخیره تنظیمات اتصال چت — فقط از مسیر سوپرادمین */
async upsertSettingsForOrganization(
organizationId: string,
role: UserRole | string,
dto: UpdateConversationSettingsRequest,
): Promise<ConversationSettingsDto> {
if (!canManageSettings(role)) {
throw new ForbiddenException('فقط مدیر می‌تواند تنظیمات گفتگو را تغییر دهد');
}
const org = await this.prisma.organization.findUnique({
where: { id: organizationId },
select: { id: true },
});
if (!org) throw new NotFoundException('محیط پیدا نشد');
const host = this.chat.normalizeHost(dto.chatHost);
if (!host) throw new BadRequestException('آدرس سرویس گفتگو نامعتبر است');
@@ -3,9 +3,66 @@ import { Cron } from '@nestjs/schedule';
import { Prisma } from '@prisma/client';
import type { LeadImportJobDto } from '@mento/shared';
import { PrismaService } from '../prisma/prisma.service';
import {
parseExcelMapping,
previewExcel,
readMappedRows,
type ExcelFieldOption,
type ExcelPreviewResult,
} from '../common/excel';
import { LeadStagesService } from './lead-stages.service';
import { LeadsService } from './leads.service';
const LEAD_ALIASES: Record<string, string> = {
title: 'title',
name: 'title',
shop: 'title',
نام: 'title',
نامفروشگاه: 'title',
فروشگاه: 'title',
عنوان: 'title',
contactname: 'contactName',
owner: 'contactName',
مالک: 'contactName',
مسئول: 'contactName',
ناممالک: 'contactName',
ناممخاطب: 'contactName',
mobile: 'mobile',
phone: 'mobile',
tel: 'mobile',
موبایل: 'mobile',
شماره: 'mobile',
شمارهتماس: 'mobile',
تلفن: 'phone',
تلفنثابت: 'phone',
city: 'city',
شهر: 'city',
region: 'region',
منطقه: 'region',
محله: 'region',
trade: 'trade',
صنف: 'trade',
رسته: 'trade',
source: 'source',
منبع: 'source',
منبعلید: 'source',
notes: 'notes',
توضیحات: 'notes',
یادداشت: 'notes',
};
const LEAD_FIELDS: ExcelFieldOption[] = [
{ key: 'title', label: 'نام / فروشگاه', required: true },
{ key: 'mobile', label: 'موبایل', required: true },
{ key: 'contactName', label: 'نام مخاطب / مالک' },
{ key: 'phone', label: 'تلفن' },
{ key: 'city', label: 'شهر' },
{ key: 'region', label: 'منطقه' },
{ key: 'trade', label: 'صنف' },
{ key: 'source', label: 'منبع' },
{ key: 'notes', label: 'توضیحات' },
];
@Injectable()
export class LeadsImportService {
private readonly logger = new Logger(LeadsImportService.name);
@@ -17,6 +74,22 @@ export class LeadsImportService {
private readonly leads: LeadsService,
) {}
preview(file: Express.Multer.File): Promise<ExcelPreviewResult> {
return previewExcel(file, LEAD_ALIASES, LEAD_FIELDS);
}
async rowsFromFile(
file: Express.Multer.File,
mappingRaw?: string,
): Promise<Array<Record<string, string>>> {
const rows = await readMappedRows(
file,
LEAD_ALIASES,
parseExcelMapping(mappingRaw),
);
return rows.map((r) => this.normalizeRow(r));
}
async enqueue(
organizationId: string,
createdById: string,
+27 -21
View File
@@ -25,7 +25,6 @@ import { CurrentUser } from '../auth/current-user.decorator';
import type { Actor } from '../common/record-scope';
import { OrgModuleGuard } from '../billing/org-module.guard';
import { RequireOrgModule } from '../billing/org-module.decorator';
import { cellStr } from '../common/excel';
import { brand } from '@mento/shared';
import {
AssignLeadsDto,
@@ -139,36 +138,43 @@ export class LeadsController {
);
}
@Post('import/preview')
@UseGuards(RolesGuard)
@Roles('owner', 'admin')
@UseInterceptors(
FileInterceptor('file', {
storage: memoryStorage(),
limits: { fileSize: 15_000_000 },
}),
)
importPreview(@UploadedFile() file?: Express.Multer.File) {
if (!file?.buffer?.length) {
throw new BadRequestException('فایل ارسال نشده');
}
return this.imports.preview(file);
}
@Post('import/file')
@UseGuards(RolesGuard)
@Roles('owner', 'admin')
@UseInterceptors(FileInterceptor('file'))
@UseInterceptors(
FileInterceptor('file', {
storage: memoryStorage(),
limits: { fileSize: 15_000_000 },
}),
)
async importFile(
@CurrentUser() user: Actor,
@UploadedFile() file?: Express.Multer.File,
@Body('mapping') mapping?: string,
) {
if (!file?.buffer?.length) {
throw new BadRequestException('فایل ارسال نشده');
}
const wb = new ExcelJS.Workbook();
await wb.xlsx.load(Uint8Array.from(file.buffer) as unknown as ExcelJS.Buffer);
const sheet = wb.worksheets[0];
if (!sheet) throw new BadRequestException('برگه اکسل خالی است');
const headerRow = sheet.getRow(1);
const headers: string[] = [];
headerRow.eachCell({ includeEmpty: true }, (cell, col) => {
headers[col - 1] = cellStr(cell.value);
});
const rows: Array<Record<string, string>> = [];
sheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
if (rowNumber === 1) return;
const obj: Record<string, string> = {};
headers.forEach((h, i) => {
if (!h) return;
obj[h] = cellStr(row.getCell(i + 1).value);
});
if (Object.values(obj).some((v) => v.trim())) rows.push(obj);
});
const rows = await this.imports.rowsFromFile(file, mapping);
if (!rows.length) {
throw new BadRequestException('ردیفی برای ایمپورت نیست');
}
return this.imports.enqueue(
user.organizationId,
user.userId,
@@ -20,6 +20,7 @@ import {
UpdatePlatformOrganizationDto,
UpdatePlatformStatusDto,
} from './dto';
import { UpdateConversationSettingsDto } from '../conversations/dto';
import { PlatformAuthGuard } from './platform-auth.guard';
import type { PlatformActor } from './platform-jwt.strategy';
import { PlatformService } from './platform.service';
@@ -129,6 +130,19 @@ export class PlatformController {
return this.platform.updateOrganizationModules(id, dto);
}
@Get('organizations/:id/conversations/settings')
getOrganizationConversationSettings(@Param('id') id: string) {
return this.conversations.getSettings(id);
}
@Patch('organizations/:id/conversations/settings')
updateOrganizationConversationSettings(
@Param('id') id: string,
@Body() dto: UpdateConversationSettingsDto,
) {
return this.conversations.upsertSettingsForOrganization(id, dto);
}
@Patch('memberships/:id')
setMembershipActive(
@Param('id') id: string,