251211:1314 Frontend: reeactor Admin panel
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-11 13:14:15 +07:00
parent c8a0f281ef
commit 3fa28bd14f
79 changed files with 6571 additions and 206 deletions

View File

@@ -0,0 +1,31 @@
import {
IsString,
IsNotEmpty,
IsBoolean,
IsOptional,
Length,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateOrganizationDto {
@ApiProperty({ example: 'ITD' })
@IsString()
@IsNotEmpty()
@Length(1, 20)
organizationCode!: string;
@ApiProperty({ example: 'Italian-Thai Development' })
@IsString()
@IsNotEmpty()
@Length(1, 255)
organizationName!: string;
@ApiProperty({ example: 1, required: false })
@IsOptional()
roleId?: number;
@ApiProperty({ example: true, required: false })
@IsOptional()
@IsBoolean()
isActive?: boolean;
}

View File

@@ -0,0 +1,36 @@
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: '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,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateOrganizationDto } from './create-organization.dto.js';
export class UpdateOrganizationDto extends PartialType(CreateOrganizationDto) {}

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

@@ -0,0 +1,45 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { OrganizationRole } from './organization-role.entity';
@Entity('organizations')
export class Organization {
@PrimaryGeneratedColumn()
id!: number;
@Column({ name: 'organization_code', length: 20, unique: true })
@Index('idx_org_code')
organizationCode!: string;
@Column({ name: 'organization_name', length: 255 })
organizationName!: string;
@Column({ name: 'role_id', nullable: true })
roleId?: number;
@Column({ name: 'is_active', default: true })
isActive!: boolean;
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date;
@DeleteDateColumn({ name: 'deleted_at' })
deletedAt!: Date;
// Relations
@ManyToOne(() => OrganizationRole, { nullable: true })
@JoinColumn({ name: 'role_id' })
organizationRole?: OrganizationRole;
}

View File

@@ -0,0 +1,63 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
UseGuards,
ParseIntPipe,
} from '@nestjs/common';
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';
@ApiTags('Organizations')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('organizations')
export class OrganizationController {
constructor(private readonly orgService: OrganizationService) {}
@Post()
@RequirePermission('master_data.manage')
@ApiOperation({ summary: 'Create Organization' })
create(@Body() dto: CreateOrganizationDto) {
return this.orgService.create(dto);
}
@Get()
@ApiOperation({ summary: 'Get All Organizations' })
findAll(@Query() query: SearchOrganizationDto) {
return this.orgService.findAll(query);
}
@Get(':id')
@ApiOperation({ summary: 'Get Organization by ID' })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.orgService.findOne(id);
}
@Patch(':id')
@RequirePermission('master_data.manage')
@ApiOperation({ summary: 'Update Organization' })
update(
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateOrganizationDto
) {
return this.orgService.update(id, dto);
}
@Delete(':id')
@RequirePermission('master_data.manage')
@ApiOperation({ summary: 'Delete Organization' })
remove(@Param('id', ParseIntPipe) id: number) {
return this.orgService.remove(id);
}
}

View File

@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { OrganizationService } from './organization.service';
import { OrganizationController } from './organization.controller';
import { Organization } from './entities/organization.entity';
import { OrganizationRole } from './entities/organization-role.entity';
@Module({
imports: [TypeOrmModule.forFeature([Organization, OrganizationRole])],
controllers: [OrganizationController],
providers: [OrganizationService],
exports: [OrganizationService],
})
export class OrganizationModule {}

View File

@@ -0,0 +1,109 @@
import {
Injectable,
NotFoundException,
ConflictException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Organization } from './entities/organization.entity';
import { CreateOrganizationDto } from './dto/create-organization.dto.js';
import { UpdateOrganizationDto } from './dto/update-organization.dto.js';
@Injectable()
export class OrganizationService {
constructor(
@InjectRepository(Organization)
private readonly orgRepo: Repository<Organization>
) {}
async create(dto: CreateOrganizationDto) {
const existing = await this.orgRepo.findOne({
where: { organizationCode: dto.organizationCode },
});
if (existing) {
throw new ConflictException(
`Organization Code "${dto.organizationCode}" already exists`
);
}
const org = this.orgRepo.create(dto);
return this.orgRepo.save(org);
}
async findAll(params?: any) {
const { search, roleId, projectId, page = 1, limit = 100 } = params || {};
const skip = (page - 1) * limit;
// Start with a basic query builder to handle dynamic conditions easily
const queryBuilder = this.orgRepo.createQueryBuilder('org');
if (search) {
queryBuilder.andWhere(
'(org.organizationCode LIKE :search OR org.organizationName LIKE :search)',
{ search: `%${search}%` }
);
}
// [Refactor] Support filtering by roleId (e.g., getting all CONTRACTORS)
if (roleId) {
// Assuming there is a relation or a way to filter by role.
// If Organization has a roleId column directly:
queryBuilder.andWhere('org.roleId = :roleId', { roleId });
}
// [New] Support filtering by projectId (e.g. organizations in a project)
// Assuming a Many-to-Many or One-to-Many relation exists via ProjectOrganization
if (projectId) {
// Use raw join to avoid circular dependency with ProjectOrganization entity
queryBuilder.innerJoin(
'project_organizations',
'po',
'po.organization_id = org.id AND po.project_id = :projectId',
{ projectId }
);
}
queryBuilder.orderBy('org.organizationCode', 'ASC').skip(skip).take(limit);
const [data, total] = await queryBuilder.getManyAndCount();
// Debug logging
console.log(`[OrganizationService] Found ${total} organizations`);
return {
data,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
async findOne(id: number) {
const org = await this.orgRepo.findOne({ where: { id } });
if (!org) throw new NotFoundException(`Organization ID ${id} not found`);
return org;
}
async update(id: number, dto: UpdateOrganizationDto) {
const org = await this.findOne(id);
Object.assign(org, dto);
return this.orgRepo.save(org);
}
async remove(id: number) {
const org = await this.findOne(id);
// Hard delete or Soft delete? Schema doesn't have deleted_at for Organization, but let's check.
// Schema says: created_at, updated_at. No deleted_at.
// So hard delete.
return this.orgRepo.remove(org);
}
async findAllActive() {
return this.orgRepo.find({
where: { isActive: true },
order: { organizationCode: 'ASC' },
});
}
}