Add Didar gap features: lost-reason reports, audit logs, tags, card assignees, and saved-views API.
Ignore Graphify local artifacts and expose new modules in superadmin.
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsIn,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import { RolesGuard } from '../auth/roles.guard';
|
||||
import { OrgModuleGuard } from '../billing/org-module.guard';
|
||||
import { RequireOrgModule } from '../billing/org-module.decorator';
|
||||
import { SavedViewsService } from './saved-views.service';
|
||||
|
||||
const SCOPES = [
|
||||
'deals',
|
||||
'people',
|
||||
'companies',
|
||||
'activities',
|
||||
'project_cards',
|
||||
] as const;
|
||||
|
||||
class CreateSavedViewDto {
|
||||
@IsString()
|
||||
@MaxLength(120)
|
||||
name!: string;
|
||||
|
||||
@IsIn(SCOPES)
|
||||
scope!: (typeof SCOPES)[number];
|
||||
|
||||
@IsObject()
|
||||
filters!: Record<string, unknown>;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isShared?: boolean;
|
||||
}
|
||||
|
||||
class UpdateSavedViewDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(120)
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(SCOPES)
|
||||
scope?: (typeof SCOPES)[number];
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
filters?: Record<string, unknown>;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isShared?: boolean;
|
||||
}
|
||||
|
||||
@Controller('saved-views')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard, OrgModuleGuard)
|
||||
@RequireOrgModule('saved_views')
|
||||
export class SavedViewsController {
|
||||
constructor(private readonly savedViews: SavedViewsService) {}
|
||||
|
||||
@Get()
|
||||
list(
|
||||
@CurrentUser() user: { organizationId: string; userId: string },
|
||||
@Query('scope') scope?: string,
|
||||
) {
|
||||
return this.savedViews.list(user.organizationId, user.userId, scope);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(
|
||||
@CurrentUser() user: { organizationId: string; userId: string },
|
||||
@Body() dto: CreateSavedViewDto,
|
||||
) {
|
||||
return this.savedViews.create(user.organizationId, user.userId, dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(
|
||||
@CurrentUser()
|
||||
user: { organizationId: string; userId: string; role: string },
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateSavedViewDto,
|
||||
) {
|
||||
const isAdmin = user.role === 'owner' || user.role === 'admin';
|
||||
return this.savedViews.update(
|
||||
user.organizationId,
|
||||
user.userId,
|
||||
id,
|
||||
dto,
|
||||
isAdmin,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async remove(
|
||||
@CurrentUser()
|
||||
user: { organizationId: string; userId: string; role: string },
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
const isAdmin = user.role === 'owner' || user.role === 'admin';
|
||||
await this.savedViews.remove(
|
||||
user.organizationId,
|
||||
user.userId,
|
||||
id,
|
||||
isAdmin,
|
||||
);
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { BillingModule } from '../billing/billing.module';
|
||||
import { SavedViewsController } from './saved-views.controller';
|
||||
import { SavedViewsService } from './saved-views.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule, BillingModule],
|
||||
controllers: [SavedViewsController],
|
||||
providers: [SavedViewsService],
|
||||
exports: [SavedViewsService],
|
||||
})
|
||||
export class SavedViewsModule {}
|
||||
@@ -0,0 +1,148 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import type {
|
||||
CreateSavedViewRequest,
|
||||
SavedViewDto,
|
||||
SavedViewScope,
|
||||
UpdateSavedViewRequest,
|
||||
} from '@mento/shared';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
const SCOPES: SavedViewScope[] = [
|
||||
'deals',
|
||||
'people',
|
||||
'companies',
|
||||
'activities',
|
||||
'project_cards',
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class SavedViewsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
scope?: string,
|
||||
): Promise<SavedViewDto[]> {
|
||||
const rows = await this.prisma.savedView.findMany({
|
||||
where: {
|
||||
organizationId,
|
||||
...(scope ? { scope } : {}),
|
||||
OR: [{ isShared: true }, { ownerUserId: userId }],
|
||||
},
|
||||
include: { owner: { select: { id: true, fullName: true } } },
|
||||
orderBy: [{ updatedAt: 'desc' }],
|
||||
});
|
||||
return rows.map((row) => this.toDto(row));
|
||||
}
|
||||
|
||||
async create(
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
dto: CreateSavedViewRequest,
|
||||
): Promise<SavedViewDto> {
|
||||
const scope = this.assertScope(dto.scope);
|
||||
const name = dto.name?.trim();
|
||||
if (!name) throw new BadRequestException('نام فیلتر الزامی است');
|
||||
const row = await this.prisma.savedView.create({
|
||||
data: {
|
||||
organizationId,
|
||||
ownerUserId: userId,
|
||||
name,
|
||||
scope,
|
||||
filters: (dto.filters ?? {}) as Prisma.InputJsonValue,
|
||||
isShared: Boolean(dto.isShared),
|
||||
},
|
||||
include: { owner: { select: { id: true, fullName: true } } },
|
||||
});
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
async update(
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
id: string,
|
||||
dto: UpdateSavedViewRequest,
|
||||
isAdmin: boolean,
|
||||
): Promise<SavedViewDto> {
|
||||
const existing = await this.prisma.savedView.findFirst({
|
||||
where: { id, organizationId },
|
||||
});
|
||||
if (!existing) throw new NotFoundException('فیلتر پیدا نشد');
|
||||
if (!isAdmin && existing.ownerUserId !== userId) {
|
||||
throw new BadRequestException('فقط مالک فیلتر میتواند آن را ویرایش کند');
|
||||
}
|
||||
|
||||
const data: Prisma.SavedViewUpdateInput = {};
|
||||
if (dto.name !== undefined) {
|
||||
const name = dto.name.trim();
|
||||
if (!name) throw new BadRequestException('نام فیلتر الزامی است');
|
||||
data.name = name;
|
||||
}
|
||||
if (dto.scope !== undefined) data.scope = this.assertScope(dto.scope);
|
||||
if (dto.filters !== undefined) {
|
||||
data.filters = dto.filters as Prisma.InputJsonValue;
|
||||
}
|
||||
if (dto.isShared !== undefined) data.isShared = Boolean(dto.isShared);
|
||||
|
||||
const row = await this.prisma.savedView.update({
|
||||
where: { id },
|
||||
data,
|
||||
include: { owner: { select: { id: true, fullName: true } } },
|
||||
});
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
async remove(
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
id: string,
|
||||
isAdmin: boolean,
|
||||
): Promise<void> {
|
||||
const existing = await this.prisma.savedView.findFirst({
|
||||
where: { id, organizationId },
|
||||
});
|
||||
if (!existing) throw new NotFoundException('فیلتر پیدا نشد');
|
||||
if (!isAdmin && existing.ownerUserId !== userId) {
|
||||
throw new BadRequestException('فقط مالک فیلتر میتواند آن را حذف کند');
|
||||
}
|
||||
await this.prisma.savedView.delete({ where: { id } });
|
||||
}
|
||||
|
||||
private assertScope(scope: string): SavedViewScope {
|
||||
if (!SCOPES.includes(scope as SavedViewScope)) {
|
||||
throw new BadRequestException('محدوده فیلتر معتبر نیست');
|
||||
}
|
||||
return scope as SavedViewScope;
|
||||
}
|
||||
|
||||
private toDto(row: {
|
||||
id: string;
|
||||
name: string;
|
||||
scope: string;
|
||||
filters: unknown;
|
||||
isShared: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
owner: { id: string; fullName: string } | null;
|
||||
}): SavedViewDto {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
scope: row.scope as SavedViewScope,
|
||||
filters:
|
||||
row.filters && typeof row.filters === 'object'
|
||||
? (row.filters as Record<string, unknown>)
|
||||
: {},
|
||||
isShared: row.isShared,
|
||||
owner: row.owner,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user