251208:0010 Backend & Frontend Debug
This commit is contained in:
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';
|
||||
Reference in New Issue
Block a user