251210:1709 Frontend: reeactor organization and run build
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled

This commit is contained in:
admin
2025-12-10 17:09:11 +07:00
parent aa96cd90e3
commit c8a0f281ef
140 changed files with 3780 additions and 1473 deletions

View File

@@ -0,0 +1,105 @@
-- Migration: Align Schema with Documentation
-- Version: 1733800000000
-- Date: 2025-12-10
-- Description: Add missing fields and fix column lengths to match schema v1.5.1
-- ==========================================================
-- Phase 1: Organizations Table Updates
-- ==========================================================
-- Add role_id column to organizations
ALTER TABLE organizations
ADD COLUMN role_id INT NULL COMMENT 'Reference to organization_roles table';
-- Add foreign key constraint
ALTER TABLE organizations
ADD CONSTRAINT fk_organizations_role FOREIGN KEY (role_id) REFERENCES organization_roles(id) ON DELETE
SET NULL;
-- Modify organization_name length from 200 to 255
ALTER TABLE organizations
MODIFY COLUMN organization_name VARCHAR(255) NOT NULL COMMENT 'Organization name';
-- ==========================================================
-- Phase 2: Users Table Updates (Security Fields)
-- ==========================================================
-- Add failed_attempts for login tracking
ALTER TABLE users
ADD COLUMN failed_attempts INT DEFAULT 0 COMMENT 'Number of failed login attempts';
-- Add locked_until for account lockout mechanism
ALTER TABLE users
ADD COLUMN locked_until DATETIME NULL COMMENT 'Account locked until this timestamp';
-- Add last_login_at for audit trail
ALTER TABLE users
ADD COLUMN last_login_at TIMESTAMP NULL COMMENT 'Last successful login timestamp';
-- ==========================================================
-- Phase 3: Roles Table Updates
-- ==========================================================
-- Modify role_name length from 50 to 100
ALTER TABLE roles
MODIFY COLUMN role_name VARCHAR(100) NOT NULL COMMENT 'Role name';
-- ==========================================================
-- Verification Queries
-- ==========================================================
-- Verify organizations table structure
SELECT COLUMN_NAME,
DATA_TYPE,
CHARACTER_MAXIMUM_LENGTH,
IS_NULLABLE,
COLUMN_COMMENT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'organizations'
ORDER BY ORDINAL_POSITION;
-- Verify users table has new security fields
SELECT COLUMN_NAME,
DATA_TYPE,
COLUMN_DEFAULT,
IS_NULLABLE,
COLUMN_COMMENT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'users'
AND COLUMN_NAME IN (
'failed_attempts',
'locked_until',
'last_login_at'
)
ORDER BY ORDINAL_POSITION;
-- Verify roles table role_name length
SELECT COLUMN_NAME,
DATA_TYPE,
CHARACTER_MAXIMUM_LENGTH
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'roles'
AND COLUMN_NAME = 'role_name';
-- ==========================================================
-- Rollback Script (Use if needed)
-- ==========================================================
/*
-- Rollback Phase 3: Roles
ALTER TABLE roles
MODIFY COLUMN role_name VARCHAR(50) NOT NULL;
-- Rollback Phase 2: Users
ALTER TABLE users
DROP COLUMN last_login_at,
DROP COLUMN locked_until,
DROP COLUMN failed_attempts;
-- Rollback Phase 1: Organizations
ALTER TABLE organizations
MODIFY COLUMN organization_name VARCHAR(200) NOT NULL;
ALTER TABLE organizations
DROP FOREIGN KEY fk_organizations_role;
ALTER TABLE organizations
DROP COLUMN role_id;
*/

View File

@@ -0,0 +1,31 @@
import { DataSource } from 'typeorm';
import { databaseConfig } from '../src/config/database.config';
import * as dotenv from 'dotenv';
import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions';
dotenv.config();
async function checkConnection() {
console.log('Checking database connection...');
console.log(`Host: ${process.env.DB_HOST}`);
console.log(`Port: ${process.env.DB_PORT}`);
console.log(`User: ${process.env.DB_USERNAME}`);
console.log(`Database: ${process.env.DB_DATABASE}`);
const dataSource = new DataSource(databaseConfig as MysqlConnectionOptions);
try {
await dataSource.initialize();
console.log('✅ Connection initialized successfully!');
const result = await dataSource.query('SHOW COLUMNS FROM rfa_types');
console.log('rfa_types columns:', result);
await dataSource.destroy();
} catch (error) {
console.error('❌ Connection failed:', error);
process.exit(1);
}
}
checkConnection();

View File

@@ -1,20 +1,15 @@
import {
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
} from 'typeorm';
import { CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm';
export abstract class BaseEntity {
// @PrimaryGeneratedColumn()
// id!: number;
@CreateDateColumn({ name: 'created_at' })
created_at!: Date;
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at' })
updated_at!: Date;
updatedAt!: Date;
@DeleteDateColumn({ name: 'deleted_at', select: false }) // select: false เพื่อซ่อน field นี้โดย Default
deleted_at!: Date;
@DeleteDateColumn({ name: 'deleted_at', select: false })
deletedAt?: Date;
}

View File

@@ -38,7 +38,7 @@ export class Role extends BaseEntity {
@PrimaryGeneratedColumn({ name: 'role_id' })
id!: number;
@Column({ name: 'role_name', length: 50, unique: true })
@Column({ name: 'role_name', length: 100, unique: true })
roleName!: string;
@Column({ name: 'description', type: 'text', nullable: true })

View File

@@ -43,6 +43,30 @@ export class MasterController {
return this.masterService.findAllCorrespondenceTypes();
}
@Post('correspondence-types')
@RequirePermission('master_data.manage')
@ApiOperation({ summary: 'Create Correspondence Type' })
createCorrespondenceType(@Body() dto: any) {
return this.masterService.createCorrespondenceType(dto);
}
@Patch('correspondence-types/:id')
@RequirePermission('master_data.manage')
@ApiOperation({ summary: 'Update Correspondence Type' })
updateCorrespondenceType(
@Param('id', ParseIntPipe) id: number,
@Body() dto: any
) {
return this.masterService.updateCorrespondenceType(id, dto);
}
@Delete('correspondence-types/:id')
@RequirePermission('master_data.manage')
@ApiOperation({ summary: 'Delete Correspondence Type' })
deleteCorrespondenceType(@Param('id', ParseIntPipe) id: number) {
return this.masterService.deleteCorrespondenceType(id);
}
@Get('correspondence-statuses')
@ApiOperation({ summary: 'Get all active correspondence statuses' })
getCorrespondenceStatuses() {
@@ -51,8 +75,33 @@ export class MasterController {
@Get('rfa-types')
@ApiOperation({ summary: 'Get all active RFA types' })
getRfaTypes() {
return this.masterService.findAllRfaTypes();
@ApiQuery({ name: 'contractId', required: false, type: Number })
getRfaTypes(@Query('contractId') contractId?: number) {
return this.masterService.findAllRfaTypes(contractId);
}
@Post('rfa-types')
@RequirePermission('master_data.manage')
@ApiOperation({ summary: 'Create RFA Type' })
createRfaType(@Body() dto: any) {
// Note: Should use proper DTO. Delegating to service.
// Need to add createRfaType to MasterService or RfaService?
// Given the context, MasterService seems appropriate for "Reference Data".
return this.masterService.createRfaType(dto);
}
@Patch('rfa-types/:id')
@RequirePermission('master_data.manage')
@ApiOperation({ summary: 'Update RFA Type' })
updateRfaType(@Param('id', ParseIntPipe) id: number, @Body() dto: any) {
return this.masterService.updateRfaType(id, dto);
}
@Delete('rfa-types/:id')
@RequirePermission('master_data.manage')
@ApiOperation({ summary: 'Delete RFA Type' })
deleteRfaType(@Param('id', ParseIntPipe) id: number) {
return this.masterService.deleteRfaType(id);
}
@Get('rfa-statuses')
@@ -108,7 +157,7 @@ export class MasterController {
@ApiQuery({ name: 'typeId', required: false, type: Number })
getSubTypes(
@Query('contractId') contractId?: number,
@Query('typeId') typeId?: number,
@Query('typeId') typeId?: number
) {
return this.masterService.findAllSubTypes(contractId, typeId);
}
@@ -136,7 +185,7 @@ export class MasterController {
@ApiOperation({ summary: 'Get numbering format for specific project/type' })
getNumberFormat(
@Query('projectId', ParseIntPipe) projectId: number,
@Query('typeId', ParseIntPipe) typeId: number,
@Query('typeId', ParseIntPipe) typeId: number
) {
return this.masterService.findNumberFormat(projectId, typeId);
}

View File

@@ -54,7 +54,7 @@ export class MasterService {
@InjectRepository(CorrespondenceSubType)
private readonly subTypeRepo: Repository<CorrespondenceSubType>,
@InjectRepository(DocumentNumberFormat)
private readonly formatRepo: Repository<DocumentNumberFormat>,
private readonly formatRepo: Repository<DocumentNumberFormat>
) {}
// ... (Method เดิม: findAllCorrespondenceTypes, findAllCorrespondenceStatuses, ฯลฯ เก็บไว้เหมือนเดิม) ...
@@ -67,18 +67,62 @@ export class MasterService {
order: { sortOrder: 'ASC' },
});
}
async createCorrespondenceType(dto: any) {
const item = this.corrTypeRepo.create(dto);
return this.corrTypeRepo.save(item);
}
async updateCorrespondenceType(id: number, dto: any) {
const item = await this.corrTypeRepo.findOne({ where: { id } });
if (!item) throw new NotFoundException('Correspondence Type not found');
Object.assign(item, dto);
return this.corrTypeRepo.save(item);
}
async deleteCorrespondenceType(id: number) {
const result = await this.corrTypeRepo.delete(id);
if (result.affected === 0)
throw new NotFoundException('Correspondence Type not found');
return { deleted: true };
}
async findAllCorrespondenceStatuses() {
return this.corrStatusRepo.find({
where: { isActive: true },
order: { sortOrder: 'ASC' },
});
}
async findAllRfaTypes() {
async findAllRfaTypes(contractId?: number) {
const where: any = { isActive: true };
if (contractId) {
where.contractId = contractId;
}
return this.rfaTypeRepo.find({
where: { isActive: true },
order: { sortOrder: 'ASC' },
where,
order: { typeCode: 'ASC' },
relations: contractId ? [] : [], // Add relations if needed later
});
}
async createRfaType(dto: any) {
// Validate unique code if needed
const rfaType = this.rfaTypeRepo.create(dto);
return this.rfaTypeRepo.save(rfaType);
}
async updateRfaType(id: number, dto: any) {
const rfaType = await this.rfaTypeRepo.findOne({ where: { id } });
if (!rfaType) throw new NotFoundException('RFA Type not found');
Object.assign(rfaType, dto);
return this.rfaTypeRepo.save(rfaType);
}
async deleteRfaType(id: number) {
const result = await this.rfaTypeRepo.delete(id);
if (result.affected === 0)
throw new NotFoundException('RFA Type not found');
return { deleted: true };
}
async findAllRfaStatuses() {
return this.rfaStatusRepo.find({
where: { isActive: true },
@@ -123,7 +167,7 @@ export class MasterService {
});
if (exists)
throw new ConflictException(
'Discipline code already exists in this contract',
'Discipline code already exists in this contract'
);
const discipline = this.disciplineRepo.create(dto);

View File

@@ -0,0 +1,23 @@
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { BaseEntity } from '../../../common/entities/base.entity';
/**
* OrganizationRole Entity
* Represents the role/type of an organization in the system
* (OWNER, DESIGNER, CONSULTANT, CONTRACTOR, THIRD_PARTY)
*
* Schema reference: organization_roles table (lines 205-211 in schema SQL)
*/
@Entity('organization_roles')
export class OrganizationRole extends BaseEntity {
@PrimaryGeneratedColumn()
id!: number;
@Column({
name: 'role_name',
length: 20,
unique: true,
comment: 'Role name (OWNER, DESIGNER, CONSULTANT, CONTRACTOR, THIRD_PARTY)'
})
roleName!: string;
}

View File

@@ -6,7 +6,10 @@ import {
UpdateDateColumn,
DeleteDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { OrganizationRole } from './organization-role.entity';
@Entity('organizations')
export class Organization {
@@ -17,9 +20,12 @@ export class Organization {
@Index('idx_org_code')
organizationCode!: string;
@Column({ name: 'organization_name', length: 200 })
@Column({ name: 'organization_name', length: 255 })
organizationName!: string;
@Column({ name: 'role_id', nullable: true })
roleId?: number;
@Column({ name: 'is_active', default: true })
isActive!: boolean;
@@ -31,4 +37,9 @@ export class Organization {
@DeleteDateColumn({ name: 'deleted_at' })
deletedAt!: Date;
// Relations
@ManyToOne(() => OrganizationRole, { nullable: true })
@JoinColumn({ name: 'role_id' })
organizationRole?: OrganizationRole;
}

View File

@@ -19,6 +19,7 @@ import {
import { ContractService } from './contract.service.js';
import { CreateContractDto } from './dto/create-contract.dto.js';
import { UpdateContractDto } from './dto/update-contract.dto.js';
import { SearchContractDto } from './dto/search-contract.dto.js';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
@@ -38,11 +39,10 @@ export class ContractController {
@Get()
@ApiOperation({
summary: 'Get All Contracts (Optional: filter by projectId)',
summary: 'Get All Contracts (Search & Filter)',
})
@ApiQuery({ name: 'projectId', required: false, type: Number })
findAll(@Query('projectId') projectId?: number) {
return this.contractService.findAll(projectId);
findAll(@Query() query: SearchContractDto) {
return this.contractService.findAll(query);
}
@Get(':id')

View File

@@ -4,7 +4,7 @@ import {
ConflictException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Repository, Like } from 'typeorm';
import { Contract } from './entities/contract.entity';
import { CreateContractDto } from './dto/create-contract.dto.js';
import { UpdateContractDto } from './dto/update-contract.dto.js';
@@ -29,17 +29,51 @@ export class ContractService {
return this.contractRepo.save(contract);
}
async findAll(projectId?: number) {
const query = this.contractRepo
.createQueryBuilder('c')
.leftJoinAndSelect('c.project', 'p')
.orderBy('c.contractCode', 'ASC');
async findAll(params?: any) {
const { search, projectId, page = 1, limit = 100 } = params || {};
const skip = (page - 1) * limit;
if (projectId) {
query.where('c.projectId = :projectId', { projectId });
const findOptions: any = {
relations: ['project'],
order: { contractCode: 'ASC' },
skip,
take: limit,
where: [],
};
const searchConditions = [];
if (search) {
searchConditions.push({ contractCode: Like(`%${search}%`) });
searchConditions.push({ contractName: Like(`%${search}%`) });
}
return query.getMany();
if (projectId) {
// Combine project filter with search if exists
if (searchConditions.length > 0) {
findOptions.where = searchConditions.map((cond) => ({
...cond,
projectId,
}));
} else {
findOptions.where = { projectId };
}
} else {
if (searchConditions.length > 0) {
findOptions.where = searchConditions;
} else {
delete findOptions.where; // No filters
}
}
const [data, total] = await this.contractRepo.findAndCount(findOptions);
return {
data,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
async findOne(id: number) {

View File

@@ -20,6 +20,10 @@ export class CreateOrganizationDto {
@Length(1, 255)
organizationName!: string;
@ApiProperty({ example: 1, required: false })
@IsOptional()
roleId?: number;
@ApiProperty({ example: true, required: false })
@IsOptional()
@IsBoolean()

View File

@@ -0,0 +1,30 @@
import { IsOptional, IsString, IsInt, Min } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiPropertyOptional } from '@nestjs/swagger';
export class SearchContractDto {
@ApiPropertyOptional({ description: 'Search term (code or name)' })
@IsOptional()
@IsString()
search?: string;
@ApiPropertyOptional({ description: 'Filter by Project ID' })
@IsOptional()
@IsInt()
@Type(() => Number)
projectId?: number;
@ApiPropertyOptional({ description: 'Page number', default: 1 })
@IsOptional()
@IsInt()
@Min(1)
@Type(() => Number)
page?: number = 1;
@ApiPropertyOptional({ description: 'Items per page', default: 100 })
@IsOptional()
@IsInt()
@Min(1)
@Type(() => Number)
limit?: number = 100;
}

View File

@@ -0,0 +1,30 @@
import { IsOptional, IsString, IsInt, Min } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiPropertyOptional } from '@nestjs/swagger';
export class SearchOrganizationDto {
@ApiPropertyOptional({ description: 'Search term (code or name)' })
@IsOptional()
@IsString()
search?: string;
@ApiPropertyOptional({ description: 'Filter by Role ID' })
@IsOptional()
@IsInt()
@Type(() => Number)
roleId?: number;
@ApiPropertyOptional({ description: 'Page number', default: 1 })
@IsOptional()
@IsInt()
@Min(1)
@Type(() => Number)
page?: number = 1;
@ApiPropertyOptional({ description: 'Items per page', default: 100 })
@IsOptional()
@IsInt()
@Min(1)
@Type(() => Number)
limit?: number = 100;
}

View File

@@ -12,6 +12,9 @@ export class Organization extends BaseEntity {
@Column({ name: 'organization_name', length: 255 })
organizationName!: string;
@Column({ name: 'role_id', nullable: true })
roleId?: number;
@Column({ name: 'is_active', default: true })
isActive!: boolean;
}

View File

@@ -6,6 +6,7 @@ import {
Patch,
Param,
Delete,
Query,
UseGuards,
ParseIntPipe,
} from '@nestjs/common';
@@ -13,6 +14,7 @@ import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { OrganizationService } from './organization.service.js';
import { CreateOrganizationDto } from './dto/create-organization.dto.js';
import { UpdateOrganizationDto } from './dto/update-organization.dto.js';
import { SearchOrganizationDto } from './dto/search-organization.dto.js';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
@@ -32,8 +34,8 @@ export class OrganizationController {
@Get()
@ApiOperation({ summary: 'Get All Organizations' })
findAll() {
return this.orgService.findAll();
findAll(@Query() query: SearchOrganizationDto) {
return this.orgService.findAll(query);
}
@Get(':id')

View File

@@ -4,7 +4,7 @@ import {
ConflictException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Repository, Like } from 'typeorm';
import { Organization } from './entities/organization.entity';
import { CreateOrganizationDto } from './dto/create-organization.dto.js';
import { UpdateOrganizationDto } from './dto/update-organization.dto.js';
@@ -29,10 +29,40 @@ export class OrganizationService {
return this.orgRepo.save(org);
}
async findAll() {
return this.orgRepo.find({
async findAll(params?: any) {
const { search, page = 1, limit = 100 } = params || {};
const skip = (page - 1) * limit;
// Use findAndCount for safer, standard TypeORM queries
const findOptions: any = {
order: { organizationCode: 'ASC' },
});
skip,
take: limit,
};
if (search) {
findOptions.where = [
{ organizationCode: Like(`%${search}%`) },
{ organizationName: Like(`%${search}%`) },
];
}
// Debug logging
console.log(
'[OrganizationService] Finding all with options:',
JSON.stringify(findOptions)
);
const [data, total] = await this.orgRepo.findAndCount(findOptions);
console.log(`[OrganizationService] Found ${total} organizations`);
return {
data,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
async findOne(id: number) {

View File

@@ -63,7 +63,7 @@ export class ProjectService {
);
}
query.orderBy('project.created_at', 'DESC');
query.orderBy('project.createdAt', 'DESC');
query.skip(skip).take(limit);
const [items, total] = await query.getManyAndCount();

View File

@@ -1,22 +1,35 @@
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
import { Entity, PrimaryGeneratedColumn, Column, AfterLoad } from 'typeorm';
@Entity('rfa_types')
export class RfaType {
@PrimaryGeneratedColumn()
id!: number;
@Column({ name: 'type_code', length: 20, unique: true })
@Column({ name: 'contract_id' })
contractId!: number;
@Column({ name: 'type_code', length: 20 })
typeCode!: string;
@Column({ name: 'type_name', length: 100 })
typeName!: string;
@Column({ name: 'type_name_th', length: 100 })
typeNameTh!: string;
@Column({ name: 'type_name_en', length: 100 })
typeNameEn!: string;
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ name: 'sort_order', default: 0 })
sortOrder!: number;
remark?: string;
@Column({ name: 'is_active', default: true })
isActive!: boolean;
// Virtual property for backward compatibility
typeName!: string;
@AfterLoad()
populateVirtualFields() {
this.typeName = this.typeNameEn;
// Map remark to description if needed, or just let description be undefined
// this['description'] = this.remark;
}
}

View File

@@ -0,0 +1,38 @@
import { IsOptional, IsString, IsInt, Min } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiPropertyOptional } from '@nestjs/swagger';
export class SearchUserDto {
@ApiPropertyOptional({
description: 'Search term (username, email, or name)',
})
@IsOptional()
@IsString()
search?: string;
@ApiPropertyOptional({ description: 'Filter by Role ID' })
@IsOptional()
@IsInt()
@Type(() => Number)
roleId?: number;
@ApiPropertyOptional({ description: 'Filter by Organization ID' })
@IsOptional()
@IsInt()
@Type(() => Number)
primaryOrganizationId?: number;
@ApiPropertyOptional({ description: 'Page number', default: 1 })
@IsOptional()
@IsInt()
@Min(1)
@Type(() => Number)
page?: number = 1;
@ApiPropertyOptional({ description: 'Items per page', default: 10 })
@IsOptional()
@IsInt()
@Min(1)
@Type(() => Number)
limit?: number = 10;
}

View File

@@ -40,6 +40,15 @@ export class User {
@Column({ name: 'is_active', default: true })
isActive!: boolean;
@Column({ name: 'failed_attempts', default: 0 })
failedAttempts!: number;
@Column({ name: 'locked_until', type: 'datetime', nullable: true })
lockedUntil?: Date;
@Column({ name: 'last_login_at', type: 'timestamp', nullable: true })
lastLoginAt?: Date;
@Column({ name: 'line_id', nullable: true, length: 100 })
lineId?: string;

View File

@@ -6,6 +6,7 @@ import {
Patch,
Param,
Delete,
Query,
UseGuards,
ParseIntPipe,
} from '@nestjs/common';
@@ -24,6 +25,7 @@ import { UserPreferenceService } from './user-preference.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { AssignRoleDto } from './dto/assign-role.dto';
import { SearchUserDto } from './dto/search-user.dto';
import { UpdatePreferenceDto } from './dto/update-preference.dto';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
@@ -106,8 +108,8 @@ export class UserController {
@ApiOperation({ summary: 'List all users' })
@ApiResponse({ status: 200, description: 'List of users' })
@RequirePermission('user.view')
findAll() {
return this.userService.findAll();
findAll(@Query() query: SearchUserDto) {
return this.userService.findAll(query);
}
@Get(':id')

View File

@@ -50,20 +50,68 @@ export class UserService {
}
}
// 2. ดึงข้อมูลทั้งหมด
async findAll(): Promise<User[]> {
return this.usersRepository.find({
select: [
'user_id',
'username',
'email',
'firstName',
'lastName',
'isActive',
'createdAt',
'updatedAt',
],
});
// 2. ดึงข้อมูลทั้งหมด (Search & Pagination)
async findAll(params?: any): Promise<any> {
const {
search,
roleId,
primaryOrganizationId,
page = 1,
limit = 100,
} = params || {};
// Create query builder
const query = this.usersRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.preference', 'preference') // Optional
.leftJoinAndSelect('user.assignments', 'assignments')
.leftJoinAndSelect('assignments.role', 'role')
.select([
'user.user_id',
'user.username',
'user.email',
'user.firstName',
'user.lastName',
'user.lineId',
'user.primaryOrganizationId',
'user.isActive',
'user.createdAt',
'user.updatedAt',
'assignments.id',
'role.roleId',
'role.roleName',
]);
// Apply Filters
if (search) {
query.andWhere(
'(user.username LIKE :search OR user.email LIKE :search OR user.firstName LIKE :search OR user.lastName LIKE :search)',
{ search: `%${search}%` }
);
}
if (primaryOrganizationId) {
query.andWhere('user.primaryOrganizationId = :orgId', {
orgId: primaryOrganizationId,
});
}
if (roleId) {
query.andWhere('role.roleId = :roleId', { roleId });
}
// Pagination
query.skip((page - 1) * limit).take(limit);
const [data, total] = await query.getManyAndCount();
return {
data,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
// 3. ดึงข้อมูลรายคน