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:
@@ -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;
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user