251216:1644 Docunment Number: Update frontend/
This commit is contained in:
@@ -37,7 +37,7 @@ export class CirculationService {
|
||||
|
||||
try {
|
||||
// Generate No. using DocumentNumberingService (Type 900 - Circulation)
|
||||
const circulationNo = await this.numberingService.generateNextNumber({
|
||||
const result = await this.numberingService.generateNextNumber({
|
||||
projectId: createDto.projectId || 0, // Use projectId from DTO or 0
|
||||
originatorId: user.primaryOrganizationId,
|
||||
typeId: 900, // Fixed Type ID for Circulation
|
||||
@@ -51,7 +51,7 @@ export class CirculationService {
|
||||
const circulation = queryRunner.manager.create(Circulation, {
|
||||
organizationId: user.primaryOrganizationId,
|
||||
correspondenceId: createDto.correspondenceId,
|
||||
circulationNo: circulationNo,
|
||||
circulationNo: result.number,
|
||||
subject: createDto.subject,
|
||||
statusCode: 'OPEN',
|
||||
createdByUserId: user.user_id,
|
||||
|
||||
@@ -1,57 +1,100 @@
|
||||
import { Controller, Post, Body, Get } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Get,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { DocumentNumberingService } from './document-numbering.service';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
|
||||
// TODO: Add Auth Guards
|
||||
@ApiTags('Admin / Document Numbering')
|
||||
@ApiBearerAuth()
|
||||
@Controller('admin/document-numbering')
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
export class DocumentNumberingAdminController {
|
||||
constructor(private readonly service: DocumentNumberingService) {}
|
||||
|
||||
@Post('manual-override')
|
||||
@ApiOperation({
|
||||
summary: 'Manually override or set a document number counter',
|
||||
})
|
||||
async manualOverride(@Body() dto: any) {
|
||||
return this.service.manualOverride(dto);
|
||||
// ----------------------------------------------------------
|
||||
// Template Management
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Get('templates')
|
||||
@ApiOperation({ summary: 'Get all document numbering templates' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async getTemplates(@Query('projectId') projectId?: number) {
|
||||
if (projectId) {
|
||||
return this.service.getTemplatesByProject(projectId);
|
||||
}
|
||||
return this.service.getTemplates();
|
||||
}
|
||||
|
||||
@Post('void-and-replace')
|
||||
@ApiOperation({ summary: 'Void a number and replace with a new generation' })
|
||||
async voidAndReplace(@Body() dto: any) {
|
||||
return this.service.voidAndReplace(dto);
|
||||
@Post('templates')
|
||||
@ApiOperation({ summary: 'Create or Update a numbering template' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async saveTemplate(@Body() dto: any) {
|
||||
return this.service.saveTemplate(dto);
|
||||
}
|
||||
|
||||
@Post('cancel')
|
||||
@ApiOperation({ summary: 'Cancel/Skip a specific document number' })
|
||||
async cancelNumber(@Body() dto: any) {
|
||||
return this.service.cancelNumber(dto);
|
||||
@Delete('templates/:id')
|
||||
@ApiOperation({ summary: 'Delete a numbering template' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async deleteTemplate(@Param('id', ParseIntPipe) id: number) {
|
||||
await this.service.deleteTemplate(id);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@Post('bulk-import')
|
||||
@ApiOperation({ summary: 'Bulk import/set document number counters' })
|
||||
async bulkImport(@Body() items: any[]) {
|
||||
return this.service.bulkImport(items);
|
||||
}
|
||||
// ----------------------------------------------------------
|
||||
// Metrics & Logs
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Get('metrics')
|
||||
@ApiOperation({ summary: 'Get numbering usage metrics and logs' })
|
||||
@RequirePermission('system.view_logs')
|
||||
async getMetrics() {
|
||||
const audit = await this.service.getAuditLogs(50);
|
||||
const errors = await this.service.getErrorLogs(50);
|
||||
return { audit, errors };
|
||||
}
|
||||
|
||||
@Get('templates')
|
||||
@ApiOperation({ summary: 'Get all document numbering templates' })
|
||||
async getTemplates() {
|
||||
return this.service.getTemplates();
|
||||
// ----------------------------------------------------------
|
||||
// Admin Operations
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Post('manual-override')
|
||||
@ApiOperation({
|
||||
summary: 'Manually override or set a document number counter',
|
||||
})
|
||||
@RequirePermission('system.manage_settings')
|
||||
async manualOverride(@Body() dto: any) {
|
||||
return this.service.manualOverride(dto);
|
||||
}
|
||||
|
||||
@Post('templates')
|
||||
@ApiOperation({ summary: 'Create or Update a numbering template' })
|
||||
async saveTemplate(@Body() dto: any) {
|
||||
// TODO: Validate DTO properly
|
||||
return this.service.saveTemplate(dto);
|
||||
@Post('void-and-replace')
|
||||
@ApiOperation({ summary: 'Void a number and replace with a new generation' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async voidAndReplace(@Body() dto: any) {
|
||||
return this.service.voidAndReplace(dto);
|
||||
}
|
||||
|
||||
@Post('cancel')
|
||||
@ApiOperation({ summary: 'Cancel/Skip a specific document number' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async cancelNumber(@Body() dto: any) {
|
||||
return this.service.cancelNumber(dto);
|
||||
}
|
||||
|
||||
@Post('bulk-import')
|
||||
@ApiOperation({ summary: 'Bulk import/set document number counters' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async bulkImport(@Body() items: any[]) {
|
||||
return this.service.bulkImport(items);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Param,
|
||||
Body,
|
||||
UseGuards,
|
||||
Query,
|
||||
ParseIntPipe,
|
||||
@@ -16,6 +20,7 @@ import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
import { DocumentNumberingService } from './document-numbering.service';
|
||||
import { PreviewNumberDto } from './dto/preview-number.dto';
|
||||
|
||||
@ApiTags('Document Numbering')
|
||||
@ApiBearerAuth()
|
||||
@@ -24,6 +29,10 @@ import { DocumentNumberingService } from './document-numbering.service';
|
||||
export class DocumentNumberingController {
|
||||
constructor(private readonly numberingService: DocumentNumberingService) {}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Logs
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Get('logs/audit')
|
||||
@ApiOperation({ summary: 'Get document generation audit logs' })
|
||||
@ApiResponse({ status: 200, description: 'List of audit logs' })
|
||||
@@ -42,12 +51,43 @@ export class DocumentNumberingController {
|
||||
return this.numberingService.getErrorLogs(limit ? Number(limit) : 100);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Sequences / Counters
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Get('sequences')
|
||||
@ApiOperation({ summary: 'Get all number sequences/counters' })
|
||||
@ApiResponse({ status: 200, description: 'List of counter sequences' })
|
||||
@ApiQuery({ name: 'projectId', required: false, type: Number })
|
||||
@RequirePermission('correspondence.read')
|
||||
getSequences(@Query('projectId') projectId?: number) {
|
||||
return this.numberingService.getSequences(
|
||||
projectId ? Number(projectId) : undefined
|
||||
);
|
||||
}
|
||||
|
||||
@Patch('counters/:id')
|
||||
@Roles(Role.ADMIN)
|
||||
@ApiOperation({ summary: 'Update counter sequence value (Admin only)' })
|
||||
@RequirePermission('system.manage_settings')
|
||||
async updateCounter(
|
||||
@Param('id') id: number,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body('sequence') sequence: number
|
||||
) {
|
||||
return this.service.setCounterValue(id, sequence);
|
||||
return this.numberingService.setCounterValue(id, sequence);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Preview / Test
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Post('preview')
|
||||
@ApiOperation({ summary: 'Preview what a document number would look like' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Preview result without incrementing counter',
|
||||
})
|
||||
@RequirePermission('correspondence.read')
|
||||
async previewNumber(@Body() dto: PreviewNumberDto) {
|
||||
return this.numberingService.previewNumber(dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,6 +270,354 @@ export class DocumentNumberingService implements OnModuleInit {
|
||||
return discipline ? discipline.code : 'GEN';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Template Management Methods
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Get all document numbering templates/formats
|
||||
*/
|
||||
async getTemplates(): Promise<DocumentNumberFormat[]> {
|
||||
try {
|
||||
return await this.formatRepo.find({
|
||||
relations: ['correspondenceType', 'project'],
|
||||
order: { projectId: 'ASC', correspondenceTypeId: 'ASC' },
|
||||
});
|
||||
} catch (error) {
|
||||
// Fallback: return without relations if there's an error
|
||||
this.logger.warn(
|
||||
'Failed to load templates with relations, trying without',
|
||||
error
|
||||
);
|
||||
return this.formatRepo.find({
|
||||
order: { projectId: 'ASC', correspondenceTypeId: 'ASC' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get templates filtered by project
|
||||
*/
|
||||
async getTemplatesByProject(
|
||||
projectId: number
|
||||
): Promise<DocumentNumberFormat[]> {
|
||||
return this.formatRepo.find({
|
||||
where: { projectId },
|
||||
relations: ['correspondenceType'],
|
||||
order: { correspondenceTypeId: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save (create or update) a template
|
||||
*/
|
||||
async saveTemplate(
|
||||
dto: Partial<DocumentNumberFormat>
|
||||
): Promise<DocumentNumberFormat> {
|
||||
if (dto.id) {
|
||||
// Update existing
|
||||
await this.formatRepo.update(dto.id, {
|
||||
formatTemplate: dto.formatTemplate,
|
||||
correspondenceTypeId: dto.correspondenceTypeId,
|
||||
description: dto.description,
|
||||
resetSequenceYearly: dto.resetSequenceYearly,
|
||||
});
|
||||
const updated = await this.formatRepo.findOne({ where: { id: dto.id } });
|
||||
if (!updated) throw new Error('Template not found after update');
|
||||
return updated;
|
||||
} else {
|
||||
// Create new
|
||||
const template = this.formatRepo.create({
|
||||
projectId: dto.projectId,
|
||||
correspondenceTypeId: dto.correspondenceTypeId ?? null,
|
||||
formatTemplate: dto.formatTemplate,
|
||||
description: dto.description,
|
||||
resetSequenceYearly: dto.resetSequenceYearly ?? true,
|
||||
});
|
||||
return this.formatRepo.save(template);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a template by ID
|
||||
*/
|
||||
async deleteTemplate(id: number): Promise<void> {
|
||||
await this.formatRepo.delete(id);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Audit & Error Log Methods
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Get audit logs for document number generation
|
||||
*/
|
||||
async getAuditLogs(limit = 100): Promise<DocumentNumberAudit[]> {
|
||||
return this.auditRepo.find({
|
||||
order: { createdAt: 'DESC' },
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error logs for document numbering
|
||||
*/
|
||||
async getErrorLogs(limit = 100): Promise<DocumentNumberError[]> {
|
||||
return this.errorRepo.find({
|
||||
order: { createdAt: 'DESC' },
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Admin Override Methods (Stubs - To be fully implemented)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Manually override/set a counter value
|
||||
* @param dto { projectId, correspondenceTypeId, year, newValue }
|
||||
*/
|
||||
async manualOverride(dto: {
|
||||
projectId: number;
|
||||
correspondenceTypeId: number | null;
|
||||
year: number;
|
||||
newValue: number;
|
||||
}): Promise<{ success: boolean; message: string }> {
|
||||
this.logger.warn(`Manual override requested: ${JSON.stringify(dto)}`);
|
||||
|
||||
const counter = await this.counterRepo.findOne({
|
||||
where: {
|
||||
projectId: dto.projectId,
|
||||
correspondenceTypeId: dto.correspondenceTypeId ?? undefined,
|
||||
currentYear: dto.year,
|
||||
},
|
||||
});
|
||||
|
||||
if (counter) {
|
||||
counter.lastNumber = dto.newValue;
|
||||
await this.counterRepo.save(counter);
|
||||
return { success: true, message: `Counter updated to ${dto.newValue}` };
|
||||
}
|
||||
|
||||
// Create new counter if not exists
|
||||
const newCounter = this.counterRepo.create({
|
||||
projectId: dto.projectId,
|
||||
correspondenceTypeId: dto.correspondenceTypeId,
|
||||
currentYear: dto.year,
|
||||
lastNumber: dto.newValue,
|
||||
version: 0,
|
||||
});
|
||||
await this.counterRepo.save(newCounter);
|
||||
return {
|
||||
success: true,
|
||||
message: `New counter created with value ${dto.newValue}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Void a document number and generate a replacement
|
||||
* @param dto { documentId, reason, context }
|
||||
*/
|
||||
async voidAndReplace(dto: {
|
||||
documentId: number;
|
||||
reason: string;
|
||||
context?: GenerateNumberContext;
|
||||
}): Promise<{ newNumber: string; auditId: number }> {
|
||||
this.logger.warn(
|
||||
`Void and replace requested for document: ${dto.documentId}`
|
||||
);
|
||||
|
||||
// 1. Find original audit record for this document
|
||||
const originalAudit = await this.auditRepo.findOne({
|
||||
where: { documentId: dto.documentId },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
if (!originalAudit) {
|
||||
throw new Error(
|
||||
`No audit record found for document ID: ${dto.documentId}`
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Create void audit record
|
||||
const voidAudit = this.auditRepo.create({
|
||||
documentId: dto.documentId,
|
||||
generatedNumber: originalAudit.generatedNumber,
|
||||
counterKey: originalAudit.counterKey,
|
||||
templateUsed: originalAudit.templateUsed,
|
||||
operation: 'VOID_REPLACE',
|
||||
metadata: {
|
||||
reason: dto.reason,
|
||||
originalAuditId: originalAudit.id,
|
||||
voidedAt: new Date().toISOString(),
|
||||
},
|
||||
userId: dto.context?.userId ?? 0,
|
||||
ipAddress: dto.context?.ipAddress,
|
||||
});
|
||||
await this.auditRepo.save(voidAudit);
|
||||
|
||||
// 3. Generate new number if context is provided
|
||||
if (dto.context) {
|
||||
const result = await this.generateNextNumber(dto.context);
|
||||
return result;
|
||||
}
|
||||
|
||||
// If no context, return info about the void operation
|
||||
return {
|
||||
newNumber: `VOIDED:${originalAudit.generatedNumber}`,
|
||||
auditId: voidAudit.id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel/skip a specific document number
|
||||
* @param dto { documentNumber, reason, userId }
|
||||
*/
|
||||
async cancelNumber(dto: {
|
||||
documentNumber: string;
|
||||
reason: string;
|
||||
userId?: number;
|
||||
ipAddress?: string;
|
||||
}): Promise<{ success: boolean; auditId: number }> {
|
||||
this.logger.warn(`Cancel number requested: ${dto.documentNumber}`);
|
||||
|
||||
// Find existing audit record for this number
|
||||
const existingAudit = await this.auditRepo.findOne({
|
||||
where: { generatedNumber: dto.documentNumber },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
// Create cancellation audit record
|
||||
const cancelAudit = this.auditRepo.create({
|
||||
documentId: existingAudit?.documentId ?? 0,
|
||||
generatedNumber: dto.documentNumber,
|
||||
counterKey: existingAudit?.counterKey ?? { cancelled: true },
|
||||
templateUsed: existingAudit?.templateUsed ?? 'CANCELLED',
|
||||
operation: 'CANCEL',
|
||||
metadata: {
|
||||
reason: dto.reason,
|
||||
cancelledAt: new Date().toISOString(),
|
||||
originalAuditId: existingAudit?.id,
|
||||
},
|
||||
userId: dto.userId ?? 0,
|
||||
ipAddress: dto.ipAddress,
|
||||
});
|
||||
|
||||
const saved = await this.auditRepo.save(cancelAudit);
|
||||
|
||||
return { success: true, auditId: saved.id };
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk import counter values
|
||||
*/
|
||||
async bulkImport(
|
||||
items: Array<{
|
||||
projectId: number;
|
||||
correspondenceTypeId: number | null;
|
||||
year: number;
|
||||
lastNumber: number;
|
||||
}>
|
||||
): Promise<{ imported: number; errors: string[] }> {
|
||||
const errors: string[] = [];
|
||||
let imported = 0;
|
||||
|
||||
for (const item of items) {
|
||||
try {
|
||||
await this.manualOverride({
|
||||
projectId: item.projectId,
|
||||
correspondenceTypeId: item.correspondenceTypeId,
|
||||
year: item.year,
|
||||
newValue: item.lastNumber,
|
||||
});
|
||||
imported++;
|
||||
} catch (e: any) {
|
||||
errors.push(`Failed to import: ${JSON.stringify(item)} - ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { imported, errors };
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Query Methods
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Get all counter sequences - for admin UI
|
||||
*/
|
||||
async getSequences(projectId?: number): Promise<
|
||||
Array<{
|
||||
projectId: number;
|
||||
originatorId: number;
|
||||
recipientOrganizationId: number;
|
||||
typeId: number;
|
||||
disciplineId: number;
|
||||
year: number;
|
||||
lastNumber: number;
|
||||
}>
|
||||
> {
|
||||
const whereClause = projectId ? { projectId } : {};
|
||||
|
||||
const counters = await this.counterRepo.find({
|
||||
where: whereClause,
|
||||
order: { year: 'DESC', lastNumber: 'DESC' },
|
||||
});
|
||||
|
||||
return counters.map((c) => ({
|
||||
projectId: c.projectId,
|
||||
originatorId: c.originatorId,
|
||||
recipientOrganizationId: c.recipientOrganizationId,
|
||||
typeId: c.typeId,
|
||||
disciplineId: c.disciplineId,
|
||||
year: c.year,
|
||||
lastNumber: c.lastNumber,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview what a document number would look like
|
||||
* WITHOUT actually incrementing the counter
|
||||
*/
|
||||
async previewNumber(
|
||||
ctx: GenerateNumberContext
|
||||
): Promise<{ previewNumber: string; nextSequence: number }> {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
// 1. Resolve Format
|
||||
const { template, resetSequenceYearly } =
|
||||
await this.resolveFormatAndScope(ctx);
|
||||
const tokens = await this.resolveTokens(ctx, currentYear);
|
||||
|
||||
// 2. Get current counter value (without incrementing)
|
||||
const counterYear = resetSequenceYearly ? currentYear : 0;
|
||||
|
||||
const existingCounter = await this.counterRepo.findOne({
|
||||
where: {
|
||||
projectId: ctx.projectId,
|
||||
originatorId: ctx.originatorId,
|
||||
typeId: ctx.typeId,
|
||||
disciplineId: ctx.disciplineId ?? 0,
|
||||
year: counterYear,
|
||||
},
|
||||
});
|
||||
|
||||
const currentSequence = existingCounter?.lastNumber ?? 0;
|
||||
const nextSequence = currentSequence + 1;
|
||||
|
||||
// 3. Generate preview number
|
||||
const previewNumber = this.replaceTokens(template, tokens, nextSequence);
|
||||
|
||||
return { previewNumber, nextSequence };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set counter value directly (for admin use)
|
||||
*/
|
||||
async setCounterValue(counterId: number, newSequence: number): Promise<void> {
|
||||
await this.counterRepo.update(counterId, { lastNumber: newSequence });
|
||||
}
|
||||
|
||||
private async logAudit(data: any): Promise<DocumentNumberAudit> {
|
||||
const audit = this.auditRepo.create({
|
||||
...data,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// File: src/modules/document-numbering/dto/preview-number.dto.ts
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class PreviewNumberDto {
|
||||
@ApiProperty({ description: 'Project ID' })
|
||||
projectId!: number;
|
||||
|
||||
@ApiProperty({ description: 'Originator organization ID' })
|
||||
originatorId!: number;
|
||||
|
||||
@ApiProperty({ description: 'Correspondence type ID' })
|
||||
typeId!: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Sub type ID (for TRANSMITTAL)' })
|
||||
subTypeId?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'RFA type ID (for RFA)' })
|
||||
rfaTypeId?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Discipline ID' })
|
||||
disciplineId?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Year (defaults to current)' })
|
||||
year?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Recipient organization ID' })
|
||||
recipientOrganizationId?: number;
|
||||
}
|
||||
Reference in New Issue
Block a user