251210:1709 Frontend: reeactor organization and run build
This commit is contained in:
@@ -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;
|
||||
*/
|
||||
31
backend/scripts/check-connection.ts
Normal file
31
backend/scripts/check-connection.ts
Normal 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();
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
30
backend/src/modules/project/dto/search-contract.dto.ts
Normal file
30
backend/src/modules/project/dto/search-contract.dto.ts
Normal 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;
|
||||
}
|
||||
30
backend/src/modules/project/dto/search-organization.dto.ts
Normal file
30
backend/src/modules/project/dto/search-organization.dto.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
38
backend/src/modules/user/dto/search-user.dto.ts
Normal file
38
backend/src/modules/user/dto/search-user.dto.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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. ดึงข้อมูลรายคน
|
||||
|
||||
Reference in New Issue
Block a user