Initial Mento CRM: portal OTP, reports, SMS, and deploy for crm.montzar.ir.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-29 21:06:30 +03:30
co-authored by Cursor
commit 29a040c21d
195 changed files with 39430 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
import {
IsBoolean,
IsInt,
IsOptional,
IsString,
MaxLength,
Min,
MinLength,
} from 'class-validator';
import { Transform, Type } from 'class-transformer';
export class CreateFaqArticleDto {
@IsString()
@MinLength(2)
@MaxLength(500)
question!: string;
@IsString()
@MinLength(2)
@MaxLength(8000)
answer!: string;
@IsOptional()
@IsString()
@MaxLength(100)
category?: string;
@IsOptional()
@IsBoolean()
isPublished?: boolean;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(0)
position?: number;
}
export class UpdateFaqArticleDto {
@IsOptional()
@IsString()
@MinLength(2)
@MaxLength(500)
question?: string;
@IsOptional()
@IsString()
@MinLength(2)
@MaxLength(8000)
answer?: string;
@IsOptional()
@IsString()
@MaxLength(100)
category?: string | null;
@IsOptional()
@IsBoolean()
isPublished?: boolean;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(0)
position?: number;
}
export class ListFaqQueryDto {
@IsOptional()
@IsString()
q?: string;
@IsOptional()
@IsString()
category?: string;
/** اگر true فقط مقالات منتشرشده */
@IsOptional()
@Transform(({ value }) => {
if (value === undefined || value === null || value === '') return undefined;
if (value === true || value === 'true' || value === '1') return true;
if (value === false || value === 'false' || value === '0') return false;
return Boolean(value);
})
@IsBoolean()
publishedOnly?: boolean;
}
+77
View File
@@ -0,0 +1,77 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
UseGuards,
} from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';
import { Roles } from '../auth/roles.decorator';
import { RolesGuard } from '../auth/roles.guard';
import {
CreateFaqArticleDto,
ListFaqQueryDto,
UpdateFaqArticleDto,
} from './dto';
import { FaqService } from './faq.service';
@Controller('faq')
@UseGuards(JwtAuthGuard, RolesGuard)
export class FaqController {
constructor(private readonly faqService: FaqService) {}
@Get()
list(
@CurrentUser() user: { organizationId: string; role: string },
@Query() query: ListFaqQueryDto,
) {
const canManage = user.role === 'owner' || user.role === 'admin';
return this.faqService.list(user.organizationId, {
q: query.q,
category: query.category,
publishedOnly: query.publishedOnly ?? !canManage,
});
}
@Get(':id')
get(
@CurrentUser() user: { organizationId: string },
@Param('id') id: string,
) {
return this.faqService.get(user.organizationId, id);
}
@Post()
@Roles('owner', 'admin')
create(
@CurrentUser() user: { organizationId: string },
@Body() dto: CreateFaqArticleDto,
) {
return this.faqService.create(user.organizationId, dto);
}
@Patch(':id')
@Roles('owner', 'admin')
update(
@CurrentUser() user: { organizationId: string },
@Param('id') id: string,
@Body() dto: UpdateFaqArticleDto,
) {
return this.faqService.update(user.organizationId, id, dto);
}
@Delete(':id')
@Roles('owner', 'admin')
async remove(
@CurrentUser() user: { organizationId: string },
@Param('id') id: string,
) {
await this.faqService.remove(user.organizationId, id);
return { ok: true };
}
}
+12
View File
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';
import { FaqController } from './faq.controller';
import { FaqService } from './faq.service';
@Module({
imports: [AuthModule],
controllers: [FaqController],
providers: [FaqService],
exports: [FaqService],
})
export class FaqModule {}
+121
View File
@@ -0,0 +1,121 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import type {
CreateFaqArticleRequest,
FaqArticleDto,
UpdateFaqArticleRequest,
} from '@mento/shared';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class FaqService {
constructor(private readonly prisma: PrismaService) {}
async list(
organizationId: string,
opts: { q?: string; category?: string; publishedOnly?: boolean },
): Promise<FaqArticleDto[]> {
const where: Prisma.FaqArticleWhereInput = {
organizationId,
...(opts.publishedOnly ? { isPublished: true } : {}),
...(opts.category?.trim()
? { category: opts.category.trim() }
: {}),
...(opts.q?.trim()
? {
OR: [
{ question: { contains: opts.q.trim(), mode: 'insensitive' } },
{ answer: { contains: opts.q.trim(), mode: 'insensitive' } },
],
}
: {}),
};
const rows = await this.prisma.faqArticle.findMany({
where,
orderBy: [{ position: 'asc' }, { createdAt: 'asc' }],
});
return rows.map(toDto);
}
async get(organizationId: string, id: string): Promise<FaqArticleDto> {
const row = await this.prisma.faqArticle.findFirst({
where: { id, organizationId },
});
if (!row) throw new NotFoundException('مقاله FAQ پیدا نشد');
return toDto(row);
}
async create(
organizationId: string,
dto: CreateFaqArticleRequest,
): Promise<FaqArticleDto> {
const row = await this.prisma.faqArticle.create({
data: {
organizationId,
question: dto.question.trim(),
answer: dto.answer.trim(),
category: emptyToNull(dto.category ?? null),
isPublished: dto.isPublished ?? true,
position: dto.position ?? 0,
},
});
return toDto(row);
}
async update(
organizationId: string,
id: string,
dto: UpdateFaqArticleRequest,
): Promise<FaqArticleDto> {
await this.ensureExists(organizationId, id);
const data: Prisma.FaqArticleUpdateInput = {};
if (dto.question !== undefined) data.question = dto.question.trim();
if (dto.answer !== undefined) data.answer = dto.answer.trim();
if ('category' in dto) data.category = emptyToNull(dto.category ?? null);
if (dto.isPublished !== undefined) data.isPublished = dto.isPublished;
if (dto.position !== undefined) data.position = dto.position;
const row = await this.prisma.faqArticle.update({ where: { id }, data });
return toDto(row);
}
async remove(organizationId: string, id: string): Promise<void> {
await this.ensureExists(organizationId, id);
await this.prisma.faqArticle.delete({ where: { id } });
}
private async ensureExists(organizationId: string, id: string) {
const found = await this.prisma.faqArticle.findFirst({
where: { id, organizationId },
select: { id: true },
});
if (!found) throw new NotFoundException('مقاله FAQ پیدا نشد');
}
}
function toDto(row: {
id: string;
question: string;
answer: string;
category: string | null;
isPublished: boolean;
position: number;
createdAt: Date;
updatedAt: Date;
}): FaqArticleDto {
return {
id: row.id,
question: row.question,
answer: row.answer,
category: row.category,
isPublished: row.isPublished,
position: row.position,
createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(),
};
}
function emptyToNull(value?: string | null): string | null {
if (value == null) return null;
const trimmed = value.trim();
return trimmed.length ? trimmed : null;
}