251208:0010 Backend & Frontend Debug
This commit is contained in:
@@ -40,6 +40,7 @@ import { DrawingModule } from './modules/drawing/drawing.module';
|
||||
import { TransmittalModule } from './modules/transmittal/transmittal.module';
|
||||
import { CirculationModule } from './modules/circulation/circulation.module';
|
||||
import { NotificationModule } from './modules/notification/notification.module';
|
||||
import { DashboardModule } from './modules/dashboard/dashboard.module';
|
||||
import { MonitoringModule } from './modules/monitoring/monitoring.module';
|
||||
import { ResilienceModule } from './common/resilience/resilience.module';
|
||||
import { SearchModule } from './modules/search/search.module';
|
||||
@@ -149,6 +150,7 @@ import { SearchModule } from './modules/search/search.module';
|
||||
CirculationModule,
|
||||
SearchModule,
|
||||
NotificationModule,
|
||||
DashboardModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
||||
@@ -20,9 +20,9 @@ import * as bcrypt from 'bcrypt';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
import { UserService } from '../../modules/user/user.service.js';
|
||||
import { User } from '../../modules/user/entities/user.entity.js';
|
||||
import { User } from '../../modules/user/entities/user.entity';
|
||||
import { RegisterDto } from './dto/register.dto.js';
|
||||
import { RefreshToken } from './entities/refresh-token.entity.js'; // [P2-2]
|
||||
import { RefreshToken } from './entities/refresh-token.entity'; // [P2-2]
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
|
||||
@@ -47,9 +47,9 @@ export class AuditLog {
|
||||
@Column({ name: 'user_agent', length: 255, nullable: true })
|
||||
userAgent?: string;
|
||||
|
||||
// ✅ [Fix] รวม Decorator ไว้ที่นี่ที่เดียว
|
||||
// ✅ [Fix] ทั้งสอง Decorator ต้องระบุ name: 'created_at'
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
@PrimaryColumn() // เพื่อบอกว่าเป็น Composite PK คู่กับ auditId
|
||||
@PrimaryColumn({ name: 'created_at' }) // Composite PK คู่กับ auditId
|
||||
createdAt!: Date;
|
||||
|
||||
// Relations
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '../../../modules/user/entities/user.entity.js';
|
||||
import { User } from '../../../modules/user/entities/user.entity';
|
||||
|
||||
@Entity('attachments')
|
||||
export class Attachment {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ScheduleModule } from '@nestjs/schedule'; // ✅ Import
|
||||
import { FileStorageService } from './file-storage.service.js';
|
||||
import { FileStorageController } from './file-storage.controller.js';
|
||||
import { FileCleanupService } from './file-cleanup.service.js'; // ✅ Import
|
||||
import { Attachment } from './entities/attachment.entity.js';
|
||||
import { Attachment } from './entities/attachment.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
||||
@@ -12,7 +12,7 @@ import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Attachment } from './entities/attachment.entity.js';
|
||||
import { Attachment } from './entities/attachment.entity';
|
||||
import { ForbiddenException } from '@nestjs/common'; // ✅ Import เพิ่ม
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -1,45 +1,54 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { User } from '../../modules/users/entities/user.entity';
|
||||
import { Role } from '../../modules/auth/entities/role.entity';
|
||||
import { User } from '../../modules/user/entities/user.entity';
|
||||
import { Role, RoleScope } from '../../modules/user/entities/role.entity';
|
||||
import { UserAssignment } from '../../modules/user/entities/user-assignment.entity';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
export async function seedUsers(dataSource: DataSource) {
|
||||
const userRepo = dataSource.getRepository(User);
|
||||
const roleRepo = dataSource.getRepository(Role);
|
||||
const assignmentRepo = dataSource.getRepository(UserAssignment);
|
||||
|
||||
// Create Roles
|
||||
const rolesData = [
|
||||
{
|
||||
roleName: 'Superadmin',
|
||||
scope: RoleScope.GLOBAL,
|
||||
description:
|
||||
'ผู้ดูแลระบบสูงสุด: สามารถทำทุกอย่างในระบบ, จัดการองค์กร, และจัดการข้อมูลหลักระดับ Global',
|
||||
},
|
||||
{
|
||||
roleName: 'Org Admin',
|
||||
scope: RoleScope.ORGANIZATION,
|
||||
description:
|
||||
'ผู้ดูแลองค์กร: จัดการผู้ใช้ในองค์กร, จัดการบทบาท / สิทธิ์ภายในองค์กร, และดูรายงานขององค์กร',
|
||||
},
|
||||
{
|
||||
roleName: 'Document Control',
|
||||
scope: RoleScope.ORGANIZATION,
|
||||
description:
|
||||
'ควบคุมเอกสารขององค์กร: เพิ่ม / แก้ไข / ลบเอกสาร, และกำหนดสิทธิ์เอกสารภายในองค์กร',
|
||||
},
|
||||
{
|
||||
roleName: 'Editor',
|
||||
scope: RoleScope.PROJECT,
|
||||
description:
|
||||
'ผู้แก้ไขเอกสารขององค์กร: เพิ่ม / แก้ไขเอกสารที่ได้รับมอบหมาย',
|
||||
},
|
||||
{
|
||||
roleName: 'Viewer',
|
||||
scope: RoleScope.PROJECT,
|
||||
description: 'ผู้ดูเอกสารขององค์กร: ดูเอกสารที่มีสิทธิ์เข้าถึงเท่านั้น',
|
||||
},
|
||||
{
|
||||
roleName: 'Project Manager',
|
||||
scope: RoleScope.PROJECT,
|
||||
description:
|
||||
'ผู้จัดการโครงการ: จัดการสมาชิกในโครงการ, สร้าง / จัดการสัญญาในโครงการ, และดูรายงานโครงการ',
|
||||
},
|
||||
{
|
||||
roleName: 'Contract Admin',
|
||||
scope: RoleScope.CONTRACT,
|
||||
description:
|
||||
'ผู้ดูแลสัญญา: จัดการสมาชิกในสัญญา, สร้าง / จัดการข้อมูลหลักเฉพาะสัญญา, และอนุมัติเอกสารในสัญญา',
|
||||
},
|
||||
@@ -49,6 +58,7 @@ export async function seedUsers(dataSource: DataSource) {
|
||||
for (const r of rolesData) {
|
||||
let role = await roleRepo.findOneBy({ roleName: r.roleName });
|
||||
if (!role) {
|
||||
// @ts-ignore
|
||||
role = await roleRepo.save(roleRepo.create(r));
|
||||
}
|
||||
roleMap.set(r.roleName, role);
|
||||
@@ -87,20 +97,30 @@ export async function seedUsers(dataSource: DataSource) {
|
||||
];
|
||||
|
||||
const salt = await bcrypt.genSalt();
|
||||
const passwordHash = await bcrypt.hash('password123', salt); // Default password
|
||||
const password = await bcrypt.hash('password123', salt); // Default password
|
||||
|
||||
for (const u of usersData) {
|
||||
const exists = await userRepo.findOneBy({ username: u.username });
|
||||
if (!exists) {
|
||||
const user = userRepo.create({
|
||||
let user = await userRepo.findOneBy({ username: u.username });
|
||||
if (!user) {
|
||||
user = userRepo.create({
|
||||
username: u.username,
|
||||
email: u.email,
|
||||
firstName: u.firstName,
|
||||
lastName: u.lastName,
|
||||
passwordHash,
|
||||
roles: [roleMap.get(u.roleName)],
|
||||
password, // Fixed: password instead of passwordHash
|
||||
});
|
||||
await userRepo.save(user);
|
||||
user = await userRepo.save(user);
|
||||
|
||||
// Create Assignment
|
||||
const role = roleMap.get(u.roleName);
|
||||
if (role) {
|
||||
const assignment = assignmentRepo.create({
|
||||
user,
|
||||
role,
|
||||
assignedAt: new Date(),
|
||||
});
|
||||
await assignmentRepo.save(assignment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,21 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CorrespondenceController } from './correspondence.controller.js';
|
||||
import { CorrespondenceService } from './correspondence.service.js';
|
||||
import { CorrespondenceRevision } from './entities/correspondence-revision.entity.js';
|
||||
import { CorrespondenceType } from './entities/correspondence-type.entity.js';
|
||||
import { Correspondence } from './entities/correspondence.entity.js';
|
||||
import { CorrespondenceRevision } from './entities/correspondence-revision.entity';
|
||||
import { CorrespondenceType } from './entities/correspondence-type.entity';
|
||||
import { Correspondence } from './entities/correspondence.entity';
|
||||
// Import Entities ใหม่
|
||||
import { CorrespondenceRouting } from './entities/correspondence-routing.entity.js';
|
||||
import { RoutingTemplateStep } from './entities/routing-template-step.entity.js';
|
||||
import { RoutingTemplate } from './entities/routing-template.entity.js';
|
||||
import { CorrespondenceRouting } from './entities/correspondence-routing.entity';
|
||||
import { RoutingTemplateStep } from './entities/routing-template-step.entity';
|
||||
import { RoutingTemplate } from './entities/routing-template.entity';
|
||||
|
||||
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module.js'; // ต้องใช้ตอน Create
|
||||
import { JsonSchemaModule } from '../json-schema/json-schema.module.js'; // ต้องใช้ Validate Details
|
||||
import { SearchModule } from '../search/search.module'; // ✅ 1. เพิ่ม Import SearchModule
|
||||
import { UserModule } from '../user/user.module.js'; // <--- 1. Import UserModule
|
||||
import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module.js'; // <--- ✅ เพิ่มบรรทัดนี้ครับ
|
||||
import { CorrespondenceReference } from './entities/correspondence-reference.entity.js';
|
||||
import { CorrespondenceStatus } from './entities/correspondence-status.entity.js';
|
||||
import { CorrespondenceReference } from './entities/correspondence-reference.entity';
|
||||
import { CorrespondenceStatus } from './entities/correspondence-status.entity';
|
||||
// Controllers & Services
|
||||
import { CorrespondenceWorkflowService } from './correspondence-workflow.service'; // Register Service นี้
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Entity, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Correspondence } from './correspondence.entity.js';
|
||||
import { Correspondence } from './correspondence.entity';
|
||||
|
||||
@Entity('correspondence_references')
|
||||
export class CorrespondenceReference {
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
CreateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
import { Correspondence } from './correspondence.entity.js';
|
||||
import { CorrespondenceStatus } from './correspondence-status.entity.js';
|
||||
import { User } from '../../user/entities/user.entity.js';
|
||||
import { Correspondence } from './correspondence.entity';
|
||||
import { CorrespondenceStatus } from './correspondence-status.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
|
||||
@Entity('correspondence_revisions')
|
||||
// ✅ เพิ่ม Index สำหรับ Virtual Columns เพื่อให้ Search เร็วขึ้น
|
||||
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
JoinColumn,
|
||||
CreateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { CorrespondenceRevision } from './correspondence-revision.entity.js';
|
||||
import { Organization } from '../../project/entities/organization.entity.js';
|
||||
import { User } from '../../user/entities/user.entity.js';
|
||||
import { RoutingTemplate } from './routing-template.entity.js';
|
||||
import { CorrespondenceRevision } from './correspondence-revision.entity';
|
||||
import { Organization } from '../../project/entities/organization.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { RoutingTemplate } from './routing-template.entity';
|
||||
|
||||
@Entity('correspondence_routings')
|
||||
export class CorrespondenceRouting {
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
DeleteDateColumn,
|
||||
CreateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Project } from '../../project/entities/project.entity.js';
|
||||
import { Organization } from '../../project/entities/organization.entity.js';
|
||||
import { CorrespondenceType } from './correspondence-type.entity.js';
|
||||
import { User } from '../../user/entities/user.entity.js';
|
||||
import { CorrespondenceRevision } from './correspondence-revision.entity.js'; // เดี๋ยวสร้าง
|
||||
import { Project } from '../../project/entities/project.entity';
|
||||
import { Organization } from '../../project/entities/organization.entity';
|
||||
import { CorrespondenceType } from './correspondence-type.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { CorrespondenceRevision } from './correspondence-revision.entity'; // เดี๋ยวสร้าง
|
||||
|
||||
@Entity('correspondences')
|
||||
export class Correspondence {
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { RoutingTemplate } from './routing-template.entity.js';
|
||||
import { Organization } from '../../project/entities/organization.entity.js';
|
||||
import { Role } from '../../user/entities/role.entity.js';
|
||||
import { RoutingTemplate } from './routing-template.entity';
|
||||
import { Organization } from '../../project/entities/organization.entity';
|
||||
import { Role } from '../../user/entities/role.entity';
|
||||
|
||||
@Entity('correspondence_routing_template_steps')
|
||||
export class RoutingTemplateStep {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { User } from '../../users/entities/user.entity';
|
||||
|
||||
@Entity('correspondences')
|
||||
export class Correspondence {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'document_number', length: 50, unique: true })
|
||||
documentNumber!: string;
|
||||
|
||||
@Column({ length: 255 })
|
||||
subject!: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
body!: string;
|
||||
|
||||
@Column({ length: 50 })
|
||||
type!: string;
|
||||
|
||||
@Column({ length: 50, default: 'Draft' })
|
||||
status!: string;
|
||||
|
||||
@Column({ name: 'created_by_id' })
|
||||
createdById!: number;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'created_by_id' })
|
||||
createdBy!: User;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
51
backend/src/modules/dashboard/dashboard.controller.ts
Normal file
51
backend/src/modules/dashboard/dashboard.controller.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// File: src/modules/dashboard/dashboard.controller.ts
|
||||
// บันทึกการแก้ไข: สร้างใหม่สำหรับ Dashboard API Endpoints
|
||||
|
||||
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
// Guards & Decorators
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
|
||||
// Service
|
||||
import { DashboardService } from './dashboard.service';
|
||||
|
||||
// DTOs
|
||||
import { GetActivityDto, GetPendingDto } from './dto';
|
||||
|
||||
@ApiTags('Dashboard')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('dashboard')
|
||||
export class DashboardController {
|
||||
constructor(private readonly dashboardService: DashboardService) {}
|
||||
|
||||
/**
|
||||
* ดึงสถิติ Dashboard
|
||||
*/
|
||||
@Get('stats')
|
||||
@ApiOperation({ summary: 'Get dashboard statistics' })
|
||||
async getStats(@CurrentUser() user: User) {
|
||||
return this.dashboardService.getStats(user.user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* ดึง Recent Activity
|
||||
*/
|
||||
@Get('activity')
|
||||
@ApiOperation({ summary: 'Get recent activity' })
|
||||
async getActivity(@CurrentUser() user: User, @Query() query: GetActivityDto) {
|
||||
return this.dashboardService.getActivity(user.user_id, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* ดึง Pending Tasks
|
||||
*/
|
||||
@Get('pending')
|
||||
@ApiOperation({ summary: 'Get pending tasks for current user' })
|
||||
async getPending(@CurrentUser() user: User, @Query() query: GetPendingDto) {
|
||||
return this.dashboardService.getPending(user.user_id, query);
|
||||
}
|
||||
}
|
||||
24
backend/src/modules/dashboard/dashboard.module.ts
Normal file
24
backend/src/modules/dashboard/dashboard.module.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// File: src/modules/dashboard/dashboard.module.ts
|
||||
// บันทึกการแก้ไข: สร้างใหม่สำหรับ Dashboard Module
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
// Entities
|
||||
import { Correspondence } from '../correspondence/entities/correspondence.entity';
|
||||
import { AuditLog } from '../../common/entities/audit-log.entity';
|
||||
import { WorkflowInstance } from '../workflow-engine/entities/workflow-instance.entity';
|
||||
|
||||
// Controller & Service
|
||||
import { DashboardController } from './dashboard.controller';
|
||||
import { DashboardService } from './dashboard.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Correspondence, AuditLog, WorkflowInstance]),
|
||||
],
|
||||
controllers: [DashboardController],
|
||||
providers: [DashboardService],
|
||||
exports: [DashboardService],
|
||||
})
|
||||
export class DashboardModule {}
|
||||
193
backend/src/modules/dashboard/dashboard.service.ts
Normal file
193
backend/src/modules/dashboard/dashboard.service.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
// File: src/modules/dashboard/dashboard.service.ts
|
||||
// บันทึกการแก้ไข: สร้างใหม่สำหรับ Dashboard Business Logic
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
// Entities
|
||||
import { Correspondence } from '../correspondence/entities/correspondence.entity';
|
||||
import { AuditLog } from '../../common/entities/audit-log.entity';
|
||||
import {
|
||||
WorkflowInstance,
|
||||
WorkflowStatus,
|
||||
} from '../workflow-engine/entities/workflow-instance.entity';
|
||||
|
||||
// DTOs
|
||||
import {
|
||||
DashboardStatsDto,
|
||||
GetActivityDto,
|
||||
ActivityItemDto,
|
||||
GetPendingDto,
|
||||
PendingTaskItemDto,
|
||||
} from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class DashboardService {
|
||||
private readonly logger = new Logger(DashboardService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Correspondence)
|
||||
private correspondenceRepo: Repository<Correspondence>,
|
||||
@InjectRepository(AuditLog)
|
||||
private auditLogRepo: Repository<AuditLog>,
|
||||
@InjectRepository(WorkflowInstance)
|
||||
private workflowInstanceRepo: Repository<WorkflowInstance>,
|
||||
private dataSource: DataSource
|
||||
) {}
|
||||
|
||||
/**
|
||||
* ดึงสถิติ Dashboard
|
||||
* @param userId - ID ของ User ที่ Login
|
||||
*/
|
||||
async getStats(userId: number): Promise<DashboardStatsDto> {
|
||||
this.logger.debug(`Getting dashboard stats for user ${userId}`);
|
||||
|
||||
// นับจำนวนเอกสารทั้งหมด
|
||||
const totalDocuments = await this.correspondenceRepo.count();
|
||||
|
||||
// นับจำนวนเอกสารเดือนนี้
|
||||
const startOfMonth = new Date();
|
||||
startOfMonth.setDate(1);
|
||||
startOfMonth.setHours(0, 0, 0, 0);
|
||||
|
||||
const documentsThisMonth = await this.correspondenceRepo
|
||||
.createQueryBuilder('c')
|
||||
.where('c.createdAt >= :startOfMonth', { startOfMonth })
|
||||
.getCount();
|
||||
|
||||
// นับงานที่รอ Approve (Workflow Active)
|
||||
const pendingApprovals = await this.workflowInstanceRepo.count({
|
||||
where: { status: WorkflowStatus.ACTIVE },
|
||||
});
|
||||
|
||||
// นับ RFA ทั้งหมด (correspondence_type_id = RFA type)
|
||||
// ใช้ Raw Query เพราะต้อง JOIN กับ correspondence_types
|
||||
const rfaCountResult = await this.dataSource.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM correspondences c
|
||||
JOIN correspondence_types ct ON c.correspondence_type_id = ct.id
|
||||
WHERE ct.type_code = 'RFA'
|
||||
`);
|
||||
const totalRfas = parseInt(rfaCountResult[0]?.count || '0', 10);
|
||||
|
||||
// นับ Circulation ทั้งหมด
|
||||
const circulationsCountResult = await this.dataSource.query(`
|
||||
SELECT COUNT(*) as count FROM circulations
|
||||
`);
|
||||
const totalCirculations = parseInt(
|
||||
circulationsCountResult[0]?.count || '0',
|
||||
10
|
||||
);
|
||||
|
||||
return {
|
||||
totalDocuments,
|
||||
documentsThisMonth,
|
||||
pendingApprovals,
|
||||
totalRfas,
|
||||
totalCirculations,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ดึง Activity ล่าสุด
|
||||
* @param userId - ID ของ User ที่ Login
|
||||
* @param dto - Query params
|
||||
*/
|
||||
async getActivity(
|
||||
userId: number,
|
||||
dto: GetActivityDto
|
||||
): Promise<ActivityItemDto[]> {
|
||||
const { limit = 10 } = dto;
|
||||
this.logger.debug(`Getting recent activity for user ${userId}`);
|
||||
|
||||
// ดึง Recent Audit Logs
|
||||
const logs = await this.auditLogRepo
|
||||
.createQueryBuilder('log')
|
||||
.leftJoin('log.user', 'user')
|
||||
.select([
|
||||
'log.action',
|
||||
'log.entityType',
|
||||
'log.entityId',
|
||||
'log.detailsJson',
|
||||
'log.createdAt',
|
||||
'user.username',
|
||||
])
|
||||
.orderBy('log.createdAt', 'DESC')
|
||||
.limit(limit)
|
||||
.getMany();
|
||||
|
||||
return logs.map((log) => ({
|
||||
action: log.action,
|
||||
entityType: log.entityType,
|
||||
entityId: log.entityId,
|
||||
details: log.detailsJson,
|
||||
createdAt: log.createdAt,
|
||||
username: log.user?.username,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* ดึง Pending Tasks ของ User
|
||||
* ใช้ v_user_tasks view จาก Database
|
||||
* @param userId - ID ของ User ที่ Login
|
||||
* @param dto - Query params
|
||||
*/
|
||||
async getPending(
|
||||
userId: number,
|
||||
dto: GetPendingDto
|
||||
): Promise<{
|
||||
data: PendingTaskItemDto[];
|
||||
meta: { total: number; page: number; limit: number };
|
||||
}> {
|
||||
const { page = 1, limit = 10 } = dto;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
this.logger.debug(`Getting pending tasks for user ${userId}`);
|
||||
|
||||
// ใช้ Raw Query เพราะต้อง Query จาก View และ Filter ด้วย JSON
|
||||
// v_user_tasks มี assignee_ids_json สำหรับ Filter
|
||||
// MariaDB 11.8: ใช้ JSON_SEARCH แทน CAST AS JSON
|
||||
const userIdNum = Number(userId);
|
||||
|
||||
const [tasks, countResult] = await Promise.all([
|
||||
this.dataSource.query(
|
||||
`
|
||||
SELECT
|
||||
instance_id as instanceId,
|
||||
workflow_code as workflowCode,
|
||||
current_state as currentState,
|
||||
entity_type as entityType,
|
||||
entity_id as entityId,
|
||||
document_number as documentNumber,
|
||||
subject,
|
||||
assigned_at as assignedAt
|
||||
FROM v_user_tasks
|
||||
WHERE
|
||||
JSON_SEARCH(assignee_ids_json, 'one', ?) IS NOT NULL
|
||||
OR owner_id = ?
|
||||
ORDER BY assigned_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`,
|
||||
[userIdNum, userIdNum, limit, offset]
|
||||
),
|
||||
this.dataSource.query(
|
||||
`
|
||||
SELECT COUNT(*) as total
|
||||
FROM v_user_tasks
|
||||
WHERE
|
||||
JSON_SEARCH(assignee_ids_json, 'one', ?) IS NOT NULL
|
||||
OR owner_id = ?
|
||||
`,
|
||||
[userIdNum, userIdNum]
|
||||
),
|
||||
]);
|
||||
|
||||
const total = parseInt(countResult[0]?.total || '0', 10);
|
||||
|
||||
return {
|
||||
data: tasks,
|
||||
meta: { total, page, limit },
|
||||
};
|
||||
}
|
||||
}
|
||||
24
backend/src/modules/dashboard/dto/dashboard-stats.dto.ts
Normal file
24
backend/src/modules/dashboard/dto/dashboard-stats.dto.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// File: src/modules/dashboard/dto/dashboard-stats.dto.ts
|
||||
// บันทึกการแก้ไข: สร้างใหม่สำหรับ Dashboard Stats Response
|
||||
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* DTO สำหรับ Response ของ Dashboard Statistics
|
||||
*/
|
||||
export class DashboardStatsDto {
|
||||
@ApiProperty({ description: 'จำนวนเอกสารทั้งหมด', example: 150 })
|
||||
totalDocuments!: number;
|
||||
|
||||
@ApiProperty({ description: 'จำนวนเอกสารเดือนนี้', example: 25 })
|
||||
documentsThisMonth!: number;
|
||||
|
||||
@ApiProperty({ description: 'จำนวนงานที่รออนุมัติ', example: 12 })
|
||||
pendingApprovals!: number;
|
||||
|
||||
@ApiProperty({ description: 'จำนวน RFA ทั้งหมด', example: 45 })
|
||||
totalRfas!: number;
|
||||
|
||||
@ApiProperty({ description: 'จำนวน Circulation ทั้งหมด', example: 30 })
|
||||
totalCirculations!: number;
|
||||
}
|
||||
42
backend/src/modules/dashboard/dto/get-activity.dto.ts
Normal file
42
backend/src/modules/dashboard/dto/get-activity.dto.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// File: src/modules/dashboard/dto/get-activity.dto.ts
|
||||
// บันทึกการแก้ไข: สร้างใหม่สำหรับ Query params ของ Activity endpoint
|
||||
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsInt, IsOptional, Max, Min } from 'class-validator';
|
||||
|
||||
/**
|
||||
* DTO สำหรับ Query params ของ GET /dashboard/activity
|
||||
*/
|
||||
export class GetActivityDto {
|
||||
@ApiPropertyOptional({ description: 'จำนวนรายการที่ต้องการ', default: 10 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(50)
|
||||
limit?: number = 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO สำหรับ Response ของ Activity Item
|
||||
*/
|
||||
export class ActivityItemDto {
|
||||
@ApiPropertyOptional({ description: 'Action ที่กระทำ' })
|
||||
action!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'ประเภท Entity' })
|
||||
entityType?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'ID ของ Entity' })
|
||||
entityId?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'รายละเอียด' })
|
||||
details?: Record<string, unknown>;
|
||||
|
||||
@ApiPropertyOptional({ description: 'วันที่กระทำ' })
|
||||
createdAt!: Date;
|
||||
|
||||
@ApiPropertyOptional({ description: 'ชื่อผู้ใช้' })
|
||||
username?: string;
|
||||
}
|
||||
55
backend/src/modules/dashboard/dto/get-pending.dto.ts
Normal file
55
backend/src/modules/dashboard/dto/get-pending.dto.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// File: src/modules/dashboard/dto/get-pending.dto.ts
|
||||
// บันทึกการแก้ไข: สร้างใหม่สำหรับ Query params ของ Pending endpoint
|
||||
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsInt, IsOptional, Max, Min } from 'class-validator';
|
||||
|
||||
/**
|
||||
* DTO สำหรับ Query params ของ GET /dashboard/pending
|
||||
*/
|
||||
export class GetPendingDto {
|
||||
@ApiPropertyOptional({ description: 'หน้าที่ต้องการ', default: 1 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number = 1;
|
||||
|
||||
@ApiPropertyOptional({ description: 'จำนวนรายการต่อหน้า', default: 10 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(50)
|
||||
limit?: number = 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO สำหรับ Response ของ Pending Task Item
|
||||
*/
|
||||
export class PendingTaskItemDto {
|
||||
@ApiPropertyOptional({ description: 'Instance ID ของ Workflow' })
|
||||
instanceId!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Workflow Code' })
|
||||
workflowCode!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'State ปัจจุบัน' })
|
||||
currentState!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'ประเภทเอกสาร' })
|
||||
entityType!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'ID ของเอกสาร' })
|
||||
entityId!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'เลขที่เอกสาร' })
|
||||
documentNumber!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'หัวข้อเรื่อง' })
|
||||
subject!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'วันที่ได้รับมอบหมาย' })
|
||||
assignedAt!: Date;
|
||||
}
|
||||
6
backend/src/modules/dashboard/dto/index.ts
Normal file
6
backend/src/modules/dashboard/dto/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// File: src/modules/dashboard/dto/index.ts
|
||||
// บันทึกการแก้ไข: สร้างใหม่สำหรับ export DTOs ทั้งหมด
|
||||
|
||||
export * from './dashboard-stats.dto';
|
||||
export * from './get-activity.dto';
|
||||
export * from './get-pending.dto';
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
JoinColumn,
|
||||
Unique,
|
||||
} from 'typeorm';
|
||||
import { Project } from '../../project/entities/project.entity.js';
|
||||
import { Project } from '../../project/entities/project.entity';
|
||||
// เรายังไม่มี CorrespondenceType Entity เดี๋ยวสร้าง Dummy ไว้ก่อน หรือข้าม Relation ไปก่อนได้
|
||||
// แต่ตามหลักควรมี CorrespondenceType (Master Data)
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '../../users/entities/user.entity';
|
||||
|
||||
@Entity('drawings')
|
||||
export class Drawing {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'drawing_number', length: 50, unique: true })
|
||||
drawingNumber!: string;
|
||||
|
||||
@Column({ length: 255 })
|
||||
title!: string;
|
||||
|
||||
@Column({ name: 'drawing_type', length: 50 })
|
||||
drawingType!: string;
|
||||
|
||||
@Column({ length: 10 })
|
||||
revision!: string;
|
||||
|
||||
@Column({ length: 50, default: 'Draft' })
|
||||
status!: string;
|
||||
|
||||
@Column({ name: 'uploaded_by_id' })
|
||||
uploadedById!: number;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'uploaded_by_id' })
|
||||
uploadedBy!: User;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -1,13 +1,47 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { JsonSchemaController } from './json-schema.controller';
|
||||
import { JsonSchemaService } from './json-schema.service';
|
||||
import { SchemaMigrationService } from './services/schema-migration.service';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
|
||||
describe('JsonSchemaController', () => {
|
||||
let controller: JsonSchemaController;
|
||||
|
||||
const mockJsonSchemaService = {
|
||||
create: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
findLatestByCode: jest.fn(),
|
||||
update: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
validateData: jest.fn(),
|
||||
processReadData: jest.fn(),
|
||||
};
|
||||
|
||||
const mockSchemaMigrationService = {
|
||||
migrateData: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [JsonSchemaController],
|
||||
}).compile();
|
||||
providers: [
|
||||
{
|
||||
provide: JsonSchemaService,
|
||||
useValue: mockJsonSchemaService,
|
||||
},
|
||||
{
|
||||
provide: SchemaMigrationService,
|
||||
useValue: mockSchemaMigrationService,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideGuard(JwtAuthGuard) // Override Guards to avoid dependency issues in Unit Test
|
||||
.useValue({ canActivate: () => true })
|
||||
.overrideGuard(RbacGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.compile();
|
||||
|
||||
controller = module.get<JsonSchemaController>(JsonSchemaController);
|
||||
});
|
||||
|
||||
@@ -58,6 +58,13 @@ export class NotificationController {
|
||||
return { data: items, meta: { total, page, limit, unreadCount } };
|
||||
}
|
||||
|
||||
@Get('unread')
|
||||
@ApiOperation({ summary: 'Get unread notification count' })
|
||||
async getUnreadCount(@CurrentUser() user: User) {
|
||||
const count = await this.notificationService.getUnreadCount(user.user_id);
|
||||
return { unreadCount: count };
|
||||
}
|
||||
|
||||
@Put(':id/read')
|
||||
@ApiOperation({ summary: 'Mark notification as read' })
|
||||
async markAsRead(
|
||||
|
||||
@@ -31,7 +31,7 @@ export class NotificationProcessor extends WorkerHost {
|
||||
private configService: ConfigService,
|
||||
private userService: UserService,
|
||||
@InjectQueue('notifications') private notificationQueue: Queue,
|
||||
@InjectRedis() private readonly redis: Redis,
|
||||
@InjectRedis() private readonly redis: Redis
|
||||
) {
|
||||
super();
|
||||
// Setup Nodemailer
|
||||
@@ -66,7 +66,7 @@ export class NotificationProcessor extends WorkerHost {
|
||||
// ✅ แก้ไขตรงนี้: Type Casting (error as Error)
|
||||
this.logger.error(
|
||||
`Failed to process job ${job.name}: ${(error as Error).message}`,
|
||||
(error as Error).stack,
|
||||
(error as Error).stack
|
||||
);
|
||||
throw error; // ให้ BullMQ จัดการ Retry
|
||||
}
|
||||
@@ -85,7 +85,7 @@ export class NotificationProcessor extends WorkerHost {
|
||||
return;
|
||||
}
|
||||
|
||||
const prefs = user.preferences || {
|
||||
const prefs = user.preference || {
|
||||
notify_email: true,
|
||||
notify_line: true,
|
||||
digest_mode: false,
|
||||
@@ -126,13 +126,13 @@ export class NotificationProcessor extends WorkerHost {
|
||||
{
|
||||
delay: this.DIGEST_DELAY,
|
||||
jobId: `digest-${data.type}-${data.userId}-${Date.now()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Set Lock ไว้ตามเวลา Delay เพื่อไม่ให้สร้าง Job ซ้ำ
|
||||
await this.redis.set(lockKey, '1', 'PX', this.DIGEST_DELAY);
|
||||
this.logger.log(
|
||||
`Scheduled digest for User ${data.userId} (${data.type}) in ${this.DIGEST_DELAY}ms`,
|
||||
`Scheduled digest for User ${data.userId} (${data.type}) in ${this.DIGEST_DELAY}ms`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,7 @@ export class NotificationProcessor extends WorkerHost {
|
||||
if (!messagesRaw || messagesRaw.length === 0) return;
|
||||
|
||||
const messages: NotificationPayload[] = messagesRaw.map((m) =>
|
||||
JSON.parse(m),
|
||||
JSON.parse(m)
|
||||
);
|
||||
const user = await this.userService.findOne(userId);
|
||||
|
||||
@@ -185,7 +185,7 @@ export class NotificationProcessor extends WorkerHost {
|
||||
const listItems = messages
|
||||
.map(
|
||||
(msg) =>
|
||||
`<li><strong>${msg.title}</strong>: ${msg.message} <a href="${msg.link}">[View]</a></li>`,
|
||||
`<li><strong>${msg.title}</strong>: ${msg.message} <a href="${msg.link}">[View]</a></li>`
|
||||
)
|
||||
.join('');
|
||||
|
||||
@@ -200,7 +200,7 @@ export class NotificationProcessor extends WorkerHost {
|
||||
`,
|
||||
});
|
||||
this.logger.log(
|
||||
`Digest Email sent to ${user.email} (${messages.length} items)`,
|
||||
`Digest Email sent to ${user.email} (${messages.length} items)`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,15 @@ export class NotificationService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ดึงจำนวน Notification ที่ยังไม่ได้อ่าน
|
||||
*/
|
||||
async getUnreadCount(userId: number): Promise<number> {
|
||||
return this.notificationRepo.count({
|
||||
where: { userId, isRead: false },
|
||||
});
|
||||
}
|
||||
|
||||
async markAsRead(id: number, userId: number): Promise<void> {
|
||||
const notification = await this.notificationRepo.findOne({
|
||||
where: { id, userId },
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Contract } from './entities/contract.entity.js';
|
||||
import { Contract } from './entities/contract.entity';
|
||||
import { CreateContractDto } from './dto/create-contract.dto.js';
|
||||
import { UpdateContractDto } from './dto/update-contract.dto.js';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Contract } from './contract.entity.js';
|
||||
import { Organization } from './organization.entity.js';
|
||||
import { Contract } from './contract.entity';
|
||||
import { Organization } from './organization.entity';
|
||||
|
||||
@Entity('contract_organizations')
|
||||
export class ContractOrganization {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Entity, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Project } from './project.entity.js';
|
||||
import { Organization } from './organization.entity.js';
|
||||
import { Project } from './project.entity';
|
||||
import { Organization } from './organization.entity';
|
||||
|
||||
@Entity('project_organizations')
|
||||
export class ProjectOrganization {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Organization } from './entities/organization.entity.js';
|
||||
import { Organization } from './entities/organization.entity';
|
||||
import { CreateOrganizationDto } from './dto/create-organization.dto.js';
|
||||
import { UpdateOrganizationDto } from './dto/update-organization.dto.js';
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ import { OrganizationController } from './organization.controller.js';
|
||||
import { ContractService } from './contract.service.js';
|
||||
import { ContractController } from './contract.controller.js';
|
||||
|
||||
import { Project } from './entities/project.entity.js';
|
||||
import { Organization } from './entities/organization.entity.js';
|
||||
import { Contract } from './entities/contract.entity.js';
|
||||
import { ProjectOrganization } from './entities/project-organization.entity.js';
|
||||
import { ContractOrganization } from './entities/contract-organization.entity.js';
|
||||
import { Project } from './entities/project.entity';
|
||||
import { Organization } from './entities/organization.entity';
|
||||
import { Contract } from './entities/contract.entity';
|
||||
import { ProjectOrganization } from './entities/project-organization.entity';
|
||||
import { ContractOrganization } from './entities/contract-organization.entity';
|
||||
// Modules
|
||||
import { UserModule } from '../user/user.module';
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, Like } from 'typeorm';
|
||||
|
||||
// Entities
|
||||
import { Project } from './entities/project.entity.js';
|
||||
import { Organization } from './entities/organization.entity.js';
|
||||
import { Project } from './entities/project.entity';
|
||||
import { Organization } from './entities/organization.entity';
|
||||
|
||||
// DTOs
|
||||
import { CreateProjectDto } from './dto/create-project.dto.js';
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '../../users/entities/user.entity';
|
||||
|
||||
@Entity('rfas')
|
||||
export class Rfa {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'rfa_number', length: 50, unique: true })
|
||||
rfaNumber!: string;
|
||||
|
||||
@Column({ length: 255 })
|
||||
title!: string;
|
||||
|
||||
@Column({ name: 'discipline_code', length: 20, nullable: true })
|
||||
disciplineCode!: string;
|
||||
|
||||
@Column({ length: 50, default: 'Draft' })
|
||||
status!: string;
|
||||
|
||||
@Column({ name: 'created_by_id' })
|
||||
createdById!: number;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'created_by_id' })
|
||||
createdBy!: User;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { UserAssignment } from './entities/user-assignment.entity.js'; // ต้องไปสร้าง Entity นี้ก่อน (ดูข้อ 3)
|
||||
import { UserAssignment } from './entities/user-assignment.entity'; // ต้องไปสร้าง Entity นี้ก่อน (ดูข้อ 3)
|
||||
import { AssignRoleDto } from './dto/assign-role.dto.js';
|
||||
import { User } from './entities/user.entity.js';
|
||||
import { User } from './entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UserAssignmentService {
|
||||
|
||||
@@ -1,18 +1,93 @@
|
||||
// File: src/modules/user/user.service.spec.ts
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { UserService } from './user.service';
|
||||
import { User } from './entities/user.entity';
|
||||
|
||||
// Mock Repository
|
||||
const mockUserRepository = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
merge: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
query: jest.fn(),
|
||||
};
|
||||
|
||||
// Mock Cache Manager
|
||||
const mockCacheManager = {
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
del: jest.fn(),
|
||||
};
|
||||
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UserService],
|
||||
providers: [
|
||||
UserService,
|
||||
{
|
||||
provide: getRepositoryToken(User),
|
||||
useValue: mockUserRepository,
|
||||
},
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: mockCacheManager,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<UserService>(UserService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return array of users', async () => {
|
||||
const mockUsers = [{ user_id: 1, username: 'test' }];
|
||||
mockUserRepository.find.mockResolvedValue(mockUsers);
|
||||
|
||||
const result = await service.findAll();
|
||||
|
||||
expect(result).toEqual(mockUsers);
|
||||
expect(mockUserRepository.find).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserPermissions', () => {
|
||||
it('should return cached permissions if available', async () => {
|
||||
const cachedPermissions = ['document.view', 'document.create'];
|
||||
mockCacheManager.get.mockResolvedValue(cachedPermissions);
|
||||
|
||||
const result = await service.getUserPermissions(1);
|
||||
|
||||
expect(result).toEqual(cachedPermissions);
|
||||
expect(mockCacheManager.get).toHaveBeenCalledWith('permissions:user:1');
|
||||
expect(mockUserRepository.query).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should query DB and cache if not in cache', async () => {
|
||||
const dbPermissions = [
|
||||
{ permission_name: 'document.view' },
|
||||
{ permission_name: 'document.create' },
|
||||
];
|
||||
mockCacheManager.get.mockResolvedValue(null);
|
||||
mockUserRepository.query.mockResolvedValue(dbPermissions);
|
||||
|
||||
const result = await service.getUserPermissions(1);
|
||||
|
||||
expect(result).toEqual(['document.view', 'document.create']);
|
||||
expect(mockCacheManager.set).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ export class UserService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private usersRepository: Repository<User>,
|
||||
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
||||
@Inject(CACHE_MANAGER) private cacheManager: Cache
|
||||
) {}
|
||||
|
||||
// 1. สร้างผู้ใช้ (Hash Password ก่อนบันทึก)
|
||||
@@ -64,7 +64,7 @@ export class UserService {
|
||||
async findOne(id: number): Promise<User> {
|
||||
const user = await this.usersRepository.findOne({
|
||||
where: { user_id: id },
|
||||
relations: ['preferences', 'roles'], // [IMPORTANT] ต้องโหลด preferences มาด้วย
|
||||
relations: ['preference', 'assignments'], // [IMPORTANT] ต้องโหลด preference มาด้วย
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
@@ -130,7 +130,7 @@ export class UserService {
|
||||
// 2. ถ้าไม่มีใน Cache ให้ Query จาก DB (View: v_user_all_permissions)
|
||||
const permissions = await this.usersRepository.query(
|
||||
`SELECT permission_name FROM v_user_all_permissions WHERE user_id = ?`,
|
||||
[userId],
|
||||
[userId]
|
||||
);
|
||||
|
||||
const permissionList = permissions.map((row: any) => row.permission_name);
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
DeleteDateColumn,
|
||||
ManyToMany,
|
||||
JoinTable,
|
||||
} from 'typeorm';
|
||||
import { Role } from '../../auth/entities/role.entity';
|
||||
|
||||
@Entity('users')
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ length: 50, unique: true })
|
||||
username!: string;
|
||||
|
||||
@Column({ length: 100, unique: true })
|
||||
email!: string;
|
||||
|
||||
@Column({ name: 'password_hash', length: 255 })
|
||||
passwordHash!: string;
|
||||
|
||||
@Column({ name: 'first_name', length: 100 })
|
||||
firstName!: string;
|
||||
|
||||
@Column({ name: 'last_name', length: 100 })
|
||||
lastName!: string;
|
||||
|
||||
@Column({ name: 'is_active', default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@ManyToMany(() => Role)
|
||||
@JoinTable({
|
||||
name: 'user_roles',
|
||||
joinColumn: { name: 'user_id', referencedColumnName: 'id' },
|
||||
inverseJoinColumn: { name: 'role_id', referencedColumnName: 'id' },
|
||||
})
|
||||
roles!: Role[];
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at' })
|
||||
deletedAt!: Date;
|
||||
}
|
||||
Reference in New Issue
Block a user