Chat settings in Super Admin and changing the chat title and position.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user