This commit is contained in:
@@ -75,7 +75,7 @@
|
||||
"socket.io": "^4.8.1",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"typeorm": "^0.3.27",
|
||||
"uuid": "^9.0.1",
|
||||
"uuid": "^11.1.0",
|
||||
"winston": "^3.18.3",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
@@ -98,7 +98,7 @@
|
||||
"@types/opossum": "^8.1.9",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Column, BeforeInsert } from 'typeorm';
|
||||
import { v7 as uuidv7 } from 'uuid';
|
||||
|
||||
/**
|
||||
* Abstract base entity providing a UUID public identifier column.
|
||||
* Uses MariaDB native UUID type (stored as BINARY(16) internally,
|
||||
* auto-converts to string format — no transformer needed).
|
||||
*
|
||||
* App generates UUIDv7 via @BeforeInsert(); DB DEFAULT UUID() is fallback.
|
||||
*
|
||||
* @see ADR-019 Hybrid Identifier Strategy
|
||||
*/
|
||||
export abstract class UuidBaseEntity {
|
||||
@Column({
|
||||
type: 'uuid',
|
||||
unique: true,
|
||||
nullable: false,
|
||||
comment: 'UUID Public Identifier (ADR-019)',
|
||||
})
|
||||
uuid!: string;
|
||||
|
||||
@BeforeInsert()
|
||||
generateUuid(): void {
|
||||
if (!this.uuid) {
|
||||
this.uuid = uuidv7();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,13 @@ import {
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '../../../modules/user/entities/user.entity';
|
||||
import { UuidBaseEntity } from '../../entities/uuid-base.entity';
|
||||
import { Exclude } from 'class-transformer';
|
||||
|
||||
@Entity('attachments')
|
||||
export class Attachment {
|
||||
export class Attachment extends UuidBaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
@Exclude()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'original_filename', length: 255 })
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { instanceToPlain } from 'class-transformer';
|
||||
|
||||
/** Metadata สำหรับ Paginated Response */
|
||||
export interface ResponseMeta {
|
||||
@@ -53,24 +54,31 @@ export class TransformInterceptor<T>
|
||||
): Observable<ApiResponse<T>> {
|
||||
return next.handle().pipe(
|
||||
map((data: T) => {
|
||||
const response = context.switchToHttp().getResponse<{ statusCode: number }>();
|
||||
const response = context
|
||||
.switchToHttp()
|
||||
.getResponse<{ statusCode: number }>();
|
||||
|
||||
// ADR-019: Serialize entities via class-transformer
|
||||
// This applies @Exclude() decorators to strip internal INT ids from responses
|
||||
const serialized = instanceToPlain(data) as T;
|
||||
|
||||
// Handle Pagination Response (Standardize)
|
||||
// ถ้า data มี structure { data: [], meta: {} } ให้ unzip ออกมา
|
||||
if (isPaginatedPayload(data)) {
|
||||
if (isPaginatedPayload(serialized)) {
|
||||
return {
|
||||
statusCode: response.statusCode,
|
||||
message: data.message ?? 'Success',
|
||||
data: data.data as unknown as T,
|
||||
meta: data.meta,
|
||||
message: serialized.message ?? 'Success',
|
||||
data: serialized.data as unknown as T,
|
||||
meta: serialized.meta,
|
||||
};
|
||||
}
|
||||
|
||||
const dataAsRecord = data as Record<string, unknown>;
|
||||
const dataAsRecord = serialized as Record<string, unknown>;
|
||||
return {
|
||||
statusCode: response.statusCode,
|
||||
message: (dataAsRecord?.['message'] as string | undefined) ?? 'Success',
|
||||
data: (dataAsRecord?.['result'] as T | undefined) ?? data,
|
||||
message:
|
||||
(dataAsRecord?.['message'] as string | undefined) ?? 'Success',
|
||||
data: (dataAsRecord?.['result'] as T | undefined) ?? serialized,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
|
||||
/**
|
||||
* Validates that a route parameter is a valid UUID string.
|
||||
* Accepts any UUID version (v1 from DB DEFAULT, v7 from app generation).
|
||||
*
|
||||
* Usage: @Param('uuid', ParseUuidPipe) uuid: string
|
||||
*
|
||||
* @see ADR-019 Hybrid Identifier Strategy
|
||||
*/
|
||||
@Injectable()
|
||||
export class ParseUuidPipe implements PipeTransform<string> {
|
||||
transform(value: string): string {
|
||||
if (!uuidValidate(value)) {
|
||||
throw new BadRequestException(`Invalid UUID format: ${value}`);
|
||||
}
|
||||
return value.toLowerCase();
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { Audit } from '../../common/decorators/audit.decorator'; // Import
|
||||
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||
|
||||
@ApiTags('Circulations')
|
||||
@ApiBearerAuth()
|
||||
@@ -45,11 +46,11 @@ export class CirculationController {
|
||||
return this.circulationService.findAll(searchDto, user);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Get(':uuid')
|
||||
@ApiOperation({ summary: 'Get circulation details' })
|
||||
@RequirePermission('document.view')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.circulationService.findOne(id);
|
||||
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||
return this.circulationService.findOneByUuid(uuid);
|
||||
}
|
||||
|
||||
@Patch('routings/:id')
|
||||
@@ -58,7 +59,7 @@ export class CirculationController {
|
||||
updateRouting(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() updateDto: UpdateCirculationRoutingDto,
|
||||
@CurrentUser() user: User,
|
||||
@CurrentUser() user: User
|
||||
) {
|
||||
return this.circulationService.updateRoutingStatus(id, updateDto, user);
|
||||
}
|
||||
|
||||
@@ -113,6 +113,17 @@ export class CirculationService {
|
||||
return circulation;
|
||||
}
|
||||
|
||||
async findOneByUuid(uuid: string) {
|
||||
const circulation = await this.circulationRepo.findOne({
|
||||
where: { uuid },
|
||||
relations: ['routings', 'routings.assignee', 'correspondence', 'creator'],
|
||||
order: { routings: { stepNumber: 'ASC' } },
|
||||
});
|
||||
if (!circulation)
|
||||
throw new NotFoundException(`Circulation UUID ${uuid} not found`);
|
||||
return circulation;
|
||||
}
|
||||
|
||||
// ✅ Logic อัปเดตสถานะและปิดงาน
|
||||
async updateRoutingStatus(
|
||||
routingId: number,
|
||||
|
||||
@@ -13,10 +13,13 @@ import { Organization } from '../../organization/entities/organization.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { CirculationStatusCode } from './circulation-status-code.entity';
|
||||
import { CirculationRouting } from './circulation-routing.entity';
|
||||
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||
import { Exclude } from 'class-transformer';
|
||||
|
||||
@Entity('circulations')
|
||||
export class Circulation {
|
||||
export class Circulation extends UuidBaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
@Exclude()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'correspondence_id', nullable: true })
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Param,
|
||||
Delete,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
@@ -22,6 +21,7 @@ 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';
|
||||
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||
|
||||
@ApiTags('Contracts')
|
||||
@ApiBearerAuth()
|
||||
@@ -45,26 +45,26 @@ export class ContractController {
|
||||
return this.contractService.findAll(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get Contract by ID' })
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.contractService.findOne(id);
|
||||
@Get(':uuid')
|
||||
@ApiOperation({ summary: 'Get Contract by UUID' })
|
||||
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||
return this.contractService.findOneByUuid(uuid);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Patch(':uuid')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Update Contract' })
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@Body() dto: UpdateContractDto
|
||||
) {
|
||||
return this.contractService.update(id, dto);
|
||||
return this.contractService.update(uuid, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Delete(':uuid')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Delete Contract' })
|
||||
remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.contractService.remove(id);
|
||||
remove(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||
return this.contractService.remove(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,15 +87,24 @@ export class ContractService {
|
||||
return contract;
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateContractDto) {
|
||||
const contract = await this.findOne(id);
|
||||
async findOneByUuid(uuid: string) {
|
||||
const contract = await this.contractRepo.findOne({
|
||||
where: { uuid },
|
||||
relations: ['project'],
|
||||
});
|
||||
if (!contract)
|
||||
throw new NotFoundException(`Contract UUID ${uuid} not found`);
|
||||
return contract;
|
||||
}
|
||||
|
||||
async update(uuid: string, dto: UpdateContractDto) {
|
||||
const contract = await this.findOneByUuid(uuid);
|
||||
Object.assign(contract, dto);
|
||||
return this.contractRepo.save(contract);
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
const contract = await this.findOne(id);
|
||||
// Schema doesn't have deleted_at for Contract either.
|
||||
async remove(uuid: string) {
|
||||
const contract = await this.findOneByUuid(uuid);
|
||||
return this.contractRepo.remove(contract);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,34 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
BeforeInsert,
|
||||
} from 'typeorm';
|
||||
import { v7 as uuidv7 } from 'uuid';
|
||||
import { Exclude } from 'class-transformer';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Project } from '../../project/entities/project.entity';
|
||||
|
||||
@Entity('contracts')
|
||||
export class Contract extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
@Exclude()
|
||||
id!: number;
|
||||
|
||||
@Column({
|
||||
type: 'uuid',
|
||||
unique: true,
|
||||
nullable: false,
|
||||
comment: 'UUID Public Identifier (ADR-019)',
|
||||
})
|
||||
uuid!: string;
|
||||
|
||||
@BeforeInsert()
|
||||
generateUuid(): void {
|
||||
if (!this.uuid) {
|
||||
this.uuid = uuidv7();
|
||||
}
|
||||
}
|
||||
|
||||
@Column({ name: 'project_id' })
|
||||
projectId!: number;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
import { Audit } from '../../common/decorators/audit.decorator';
|
||||
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||
|
||||
@ApiTags('Correspondences')
|
||||
@Controller('correspondences')
|
||||
@@ -119,7 +120,7 @@ export class CorrespondenceController {
|
||||
return this.correspondenceService.findAll(searchDto);
|
||||
}
|
||||
|
||||
@Post(':id/submit')
|
||||
@Post(':uuid/submit')
|
||||
@ApiOperation({ summary: 'Submit correspondence to Unified Workflow Engine' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
@@ -127,8 +128,8 @@ export class CorrespondenceController {
|
||||
})
|
||||
@RequirePermission('correspondence.create')
|
||||
@Audit('correspondence.submit', 'correspondence')
|
||||
submit(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
async submit(
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@Body() submitDto: SubmitCorrespondenceDto,
|
||||
@Request()
|
||||
req: Request & {
|
||||
@@ -138,28 +139,29 @@ export class CorrespondenceController {
|
||||
};
|
||||
}
|
||||
) {
|
||||
const corr = await this.correspondenceService.findOneByUuid(uuid);
|
||||
// Extract roles from user assignments
|
||||
const userRoles =
|
||||
req.user.assignments?.map((a) => a.role?.roleName).filter(Boolean) || [];
|
||||
|
||||
// Use Unified Workflow Engine - pass user roles for DSL requirements check
|
||||
return this.workflowService.submitWorkflow(
|
||||
id,
|
||||
corr.id,
|
||||
req.user.user_id,
|
||||
userRoles,
|
||||
submitDto.note
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get correspondence by ID' })
|
||||
@Get(':uuid')
|
||||
@ApiOperation({ summary: 'Get correspondence by UUID' })
|
||||
@ApiResponse({ status: 200, description: 'Return correspondence details.' })
|
||||
@RequirePermission('document.view')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.correspondenceService.findOne(id);
|
||||
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||
return this.correspondenceService.findOneByUuid(uuid);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Put(':uuid')
|
||||
@ApiOperation({ summary: 'Update correspondence (Draft only)' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
@@ -167,48 +169,52 @@ export class CorrespondenceController {
|
||||
})
|
||||
@RequirePermission('correspondence.create') // Assuming create permission is enough for draft update, or add 'correspondence.edit'
|
||||
@Audit('correspondence.update', 'correspondence')
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
async update(
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@Body() updateDto: UpdateCorrespondenceDto,
|
||||
@Request() req: Request & { user: unknown }
|
||||
) {
|
||||
const corr = await this.correspondenceService.findOneByUuid(uuid);
|
||||
return this.correspondenceService.update(
|
||||
id,
|
||||
corr.id,
|
||||
updateDto,
|
||||
req.user as Parameters<typeof this.correspondenceService.create>[1]
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id/references')
|
||||
@Get(':uuid/references')
|
||||
@ApiOperation({ summary: 'Get referenced documents' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Return list of referenced documents.',
|
||||
})
|
||||
@RequirePermission('document.view')
|
||||
getReferences(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.correspondenceService.getReferences(id);
|
||||
async getReferences(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||
const corr = await this.correspondenceService.findOneByUuid(uuid);
|
||||
return this.correspondenceService.getReferences(corr.id);
|
||||
}
|
||||
|
||||
@Post(':id/references')
|
||||
@Post(':uuid/references')
|
||||
@ApiOperation({ summary: 'Add reference to another document' })
|
||||
@ApiResponse({ status: 201, description: 'Reference added successfully.' })
|
||||
@RequirePermission('document.edit')
|
||||
addReference(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
async addReference(
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@Body() dto: AddReferenceDto
|
||||
) {
|
||||
return this.correspondenceService.addReference(id, dto);
|
||||
const corr = await this.correspondenceService.findOneByUuid(uuid);
|
||||
return this.correspondenceService.addReference(corr.id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id/references/:targetId')
|
||||
@Delete(':uuid/references/:targetId')
|
||||
@ApiOperation({ summary: 'Remove reference' })
|
||||
@ApiResponse({ status: 200, description: 'Reference removed successfully.' })
|
||||
@RequirePermission('document.edit')
|
||||
removeReference(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
async removeReference(
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@Param('targetId', ParseIntPipe) targetId: number
|
||||
) {
|
||||
return this.correspondenceService.removeReference(id, targetId);
|
||||
const corr = await this.correspondenceService.findOneByUuid(uuid);
|
||||
return this.correspondenceService.removeReference(corr.id, targetId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,6 +333,26 @@ export class CorrespondenceService {
|
||||
return correspondence;
|
||||
}
|
||||
|
||||
async findOneByUuid(uuid: string) {
|
||||
const correspondence = await this.correspondenceRepo.findOne({
|
||||
where: { uuid },
|
||||
relations: [
|
||||
'revisions',
|
||||
'revisions.status',
|
||||
'type',
|
||||
'project',
|
||||
'originator',
|
||||
'recipients',
|
||||
'recipients.recipientOrganization',
|
||||
],
|
||||
});
|
||||
|
||||
if (!correspondence) {
|
||||
throw new NotFoundException(`Correspondence with UUID ${uuid} not found`);
|
||||
}
|
||||
return correspondence;
|
||||
}
|
||||
|
||||
async addReference(id: number, dto: AddReferenceDto) {
|
||||
const source = await this.correspondenceRepo.findOne({ where: { id } });
|
||||
const target = await this.correspondenceRepo.findOne({
|
||||
|
||||
@@ -13,13 +13,16 @@ import { RfaRevision } from '../../rfa/entities/rfa-revision.entity';
|
||||
import { Correspondence } from './correspondence.entity';
|
||||
import { CorrespondenceStatus } from './correspondence-status.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||
import { Exclude } from 'class-transformer';
|
||||
|
||||
@Entity('correspondence_revisions')
|
||||
// ✅ เพิ่ม Index สำหรับ Virtual Columns เพื่อให้ Search เร็วขึ้น
|
||||
@Index('idx_corr_rev_v_project', ['vRefProjectId'])
|
||||
@Index('idx_corr_rev_v_type', ['vRefType'])
|
||||
export class CorrespondenceRevision {
|
||||
export class CorrespondenceRevision extends UuidBaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
@Exclude()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'correspondence_id' })
|
||||
|
||||
@@ -15,10 +15,13 @@ import { User } from '../../user/entities/user.entity';
|
||||
import { CorrespondenceRecipient } from './correspondence-recipient.entity';
|
||||
import { CorrespondenceRevision } from './correspondence-revision.entity';
|
||||
import { Discipline } from '../../master/entities/discipline.entity';
|
||||
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||
import { Exclude } from 'class-transformer';
|
||||
|
||||
@Entity('correspondences')
|
||||
export class Correspondence {
|
||||
export class Correspondence extends UuidBaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
@Exclude()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'correspondence_number', length: 100 })
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
ParseIntPipe,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
@@ -34,6 +33,7 @@ import { SearchAsBuiltDrawingDto } from './dto/search-asbuilt-drawing.dto';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
import { Audit } from '../../common/decorators/audit.decorator';
|
||||
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
|
||||
@ApiTags('Drawings - AS Built')
|
||||
@@ -56,16 +56,17 @@ export class AsBuiltDrawingController {
|
||||
return this.asBuiltDrawingService.create(createDto, user);
|
||||
}
|
||||
|
||||
@Post(':id/revisions')
|
||||
@Post(':uuid/revisions')
|
||||
@ApiOperation({ summary: 'Create new revision for AS Built Drawing' })
|
||||
@ApiResponse({ status: 201, description: 'Revision created' })
|
||||
@ApiResponse({ status: 404, description: 'AS Built Drawing not found' })
|
||||
@ApiResponse({ status: 409, description: 'Revision label already exists' })
|
||||
async createRevision(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@Body() createDto: CreateAsBuiltDrawingRevisionDto
|
||||
) {
|
||||
return this.asBuiltDrawingService.createRevision(id, createDto);
|
||||
const drawing = await this.asBuiltDrawingService.findOneByUuid(uuid);
|
||||
return this.asBuiltDrawingService.createRevision(drawing.id, createDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@@ -76,16 +77,16 @@ export class AsBuiltDrawingController {
|
||||
return this.asBuiltDrawingService.findAll(searchDto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get AS Built Drawing by ID' })
|
||||
@Get(':uuid')
|
||||
@ApiOperation({ summary: 'Get AS Built Drawing by UUID' })
|
||||
@ApiResponse({ status: 200, description: 'AS Built Drawing details' })
|
||||
@ApiResponse({ status: 404, description: 'AS Built Drawing not found' })
|
||||
@RequirePermission('drawing.view')
|
||||
async findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.asBuiltDrawingService.findOne(id);
|
||||
async findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||
return this.asBuiltDrawingService.findOneByUuid(uuid);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Delete(':uuid')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@ApiOperation({ summary: 'Soft delete AS Built Drawing' })
|
||||
@ApiResponse({ status: 204, description: 'AS Built Drawing deleted' })
|
||||
@@ -93,9 +94,10 @@ export class AsBuiltDrawingController {
|
||||
@RequirePermission('drawing.delete')
|
||||
@Audit('drawing.delete', 'asbuilt_drawing')
|
||||
async remove(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@CurrentUser() user: User
|
||||
) {
|
||||
return this.asBuiltDrawingService.remove(id, user);
|
||||
const drawing = await this.asBuiltDrawingService.findOneByUuid(uuid);
|
||||
return this.asBuiltDrawingService.remove(drawing.id, user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,6 +295,28 @@ export class AsBuiltDrawingService {
|
||||
return asBuiltDrawing;
|
||||
}
|
||||
|
||||
async findOneByUuid(uuid: string) {
|
||||
const asBuiltDrawing = await this.asBuiltDrawingRepo.findOne({
|
||||
where: { uuid },
|
||||
relations: [
|
||||
'mainCategory',
|
||||
'subCategory',
|
||||
'revisions',
|
||||
'revisions.attachments',
|
||||
'revisions.shopDrawingRevisions',
|
||||
],
|
||||
order: {
|
||||
revisions: { revisionNumber: 'DESC' },
|
||||
},
|
||||
});
|
||||
|
||||
if (!asBuiltDrawing) {
|
||||
throw new NotFoundException(`AS Built Drawing UUID ${uuid} not found`);
|
||||
}
|
||||
|
||||
return asBuiltDrawing;
|
||||
}
|
||||
|
||||
/**
|
||||
* ลบ AS Built Drawing
|
||||
*/
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
@@ -20,6 +19,7 @@ import { SearchContractDrawingDto } from './dto/search-contract-drawing.dto';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
|
||||
@@ -51,28 +51,33 @@ export class ContractDrawingController {
|
||||
return this.contractDrawingService.findAll(searchDto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Get(':uuid')
|
||||
@ApiOperation({ summary: 'Get Contract Drawing details' })
|
||||
@RequirePermission('document.view')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.contractDrawingService.findOne(id);
|
||||
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||
return this.contractDrawingService.findOneByUuid(uuid);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Put(':uuid')
|
||||
@ApiOperation({ summary: 'Update Contract Drawing' })
|
||||
@RequirePermission('drawing.create') // สิทธิ์ ID 39 ครอบคลุมการแก้ไขด้วย
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
async update(
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@Body() updateDto: UpdateContractDrawingDto,
|
||||
@CurrentUser() user: User
|
||||
) {
|
||||
return this.contractDrawingService.update(id, updateDto, user);
|
||||
const drawing = await this.contractDrawingService.findOneByUuid(uuid);
|
||||
return this.contractDrawingService.update(drawing.id, updateDto, user);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Delete(':uuid')
|
||||
@ApiOperation({ summary: 'Delete Contract Drawing (Soft Delete)' })
|
||||
@RequirePermission('document.delete') // สิทธิ์ ID 34: ลบเอกสาร
|
||||
remove(@Param('id', ParseIntPipe) id: number, @CurrentUser() user: User) {
|
||||
return this.contractDrawingService.remove(id, user);
|
||||
async remove(
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@CurrentUser() user: User
|
||||
) {
|
||||
const drawing = await this.contractDrawingService.findOneByUuid(uuid);
|
||||
return this.contractDrawingService.remove(drawing.id, user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +180,19 @@ export class ContractDrawingService {
|
||||
return drawing;
|
||||
}
|
||||
|
||||
async findOneByUuid(uuid: string) {
|
||||
const drawing = await this.drawingRepo.findOne({
|
||||
where: { uuid },
|
||||
relations: ['attachments'],
|
||||
});
|
||||
|
||||
if (!drawing) {
|
||||
throw new NotFoundException(`Contract Drawing UUID ${uuid} not found`);
|
||||
}
|
||||
|
||||
return drawing;
|
||||
}
|
||||
|
||||
/**
|
||||
* แก้ไขข้อมูลแบบ (Update)
|
||||
*/
|
||||
|
||||
@@ -13,11 +13,14 @@ import { AsBuiltDrawing } from './asbuilt-drawing.entity';
|
||||
import { ShopDrawingRevision } from './shop-drawing-revision.entity';
|
||||
import { Attachment } from '../../../common/file-storage/entities/attachment.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||
import { Exclude } from 'class-transformer';
|
||||
|
||||
@Entity('asbuilt_drawing_revisions')
|
||||
@Unique(['asBuiltDrawingId', 'isCurrent'])
|
||||
export class AsBuiltDrawingRevision {
|
||||
export class AsBuiltDrawingRevision extends UuidBaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
@Exclude()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'asbuilt_drawing_id' })
|
||||
|
||||
@@ -14,10 +14,13 @@ import { AsBuiltDrawingRevision } from './asbuilt-drawing-revision.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { ShopDrawingMainCategory } from './shop-drawing-main-category.entity';
|
||||
import { ShopDrawingSubCategory } from './shop-drawing-sub-category.entity';
|
||||
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||
import { Exclude } from 'class-transformer';
|
||||
|
||||
@Entity('asbuilt_drawings')
|
||||
export class AsBuiltDrawing {
|
||||
export class AsBuiltDrawing extends UuidBaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
@Exclude()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'project_id' })
|
||||
|
||||
@@ -15,10 +15,13 @@ import { User } from '../../user/entities/user.entity';
|
||||
import { Attachment } from '../../../common/file-storage/entities/attachment.entity';
|
||||
import { ContractDrawingSubcatCatMap } from './contract-drawing-subcat-cat-map.entity';
|
||||
import { ContractDrawingVolume } from './contract-drawing-volume.entity';
|
||||
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||
import { Exclude } from 'class-transformer';
|
||||
|
||||
@Entity('contract_drawings')
|
||||
export class ContractDrawing {
|
||||
export class ContractDrawing extends UuidBaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
@Exclude()
|
||||
id!: number; // ! ห้ามว่าง
|
||||
|
||||
@Column({ name: 'project_id' })
|
||||
|
||||
@@ -13,11 +13,14 @@ import { ShopDrawing } from './shop-drawing.entity';
|
||||
import { ContractDrawing } from './contract-drawing.entity';
|
||||
import { Attachment } from '../../../common/file-storage/entities/attachment.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||
import { Exclude } from 'class-transformer';
|
||||
|
||||
@Entity('shop_drawing_revisions')
|
||||
@Unique(['shopDrawingId', 'isCurrent'])
|
||||
export class ShopDrawingRevision {
|
||||
export class ShopDrawingRevision extends UuidBaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
@Exclude()
|
||||
id!: number; // เติม !
|
||||
|
||||
@Column({ name: 'shop_drawing_id' })
|
||||
|
||||
@@ -13,10 +13,13 @@ import { ShopDrawingRevision } from './shop-drawing-revision.entity';
|
||||
import { Project } from '../../project/entities/project.entity';
|
||||
import { ShopDrawingMainCategory } from './shop-drawing-main-category.entity';
|
||||
import { ShopDrawingSubCategory } from './shop-drawing-sub-category.entity';
|
||||
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||
import { Exclude } from 'class-transformer';
|
||||
|
||||
@Entity('shop_drawings')
|
||||
export class ShopDrawing {
|
||||
export class ShopDrawing extends UuidBaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
@Exclude()
|
||||
id!: number; // เติม !
|
||||
|
||||
@Column({ name: 'project_id' })
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
@@ -18,6 +17,7 @@ import { CreateShopDrawingRevisionDto } from './dto/create-shop-drawing-revision
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { Audit } from '../../common/decorators/audit.decorator'; // Import
|
||||
@@ -44,21 +44,22 @@ export class ShopDrawingController {
|
||||
return this.shopDrawingService.findAll(searchDto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Get(':uuid')
|
||||
@ApiOperation({ summary: 'Get Shop Drawing details with revisions' })
|
||||
@RequirePermission('drawing.view')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.shopDrawingService.findOne(id);
|
||||
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||
return this.shopDrawingService.findOneByUuid(uuid);
|
||||
}
|
||||
|
||||
@Post(':id/revisions')
|
||||
@Post(':uuid/revisions')
|
||||
@ApiOperation({ summary: 'Add new revision to existing Shop Drawing' })
|
||||
@RequirePermission('drawing.create') // หรือ drawing.edit ตาม Logic องค์กร
|
||||
@Audit('drawing.create', 'shop_drawing') // ✅ แปะตรงนี้
|
||||
createRevision(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() createRevisionDto: CreateShopDrawingRevisionDto,
|
||||
async createRevision(
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@Body() createRevisionDto: CreateShopDrawingRevisionDto
|
||||
) {
|
||||
return this.shopDrawingService.createRevision(id, createRevisionDto);
|
||||
const sd = await this.shopDrawingService.findOneByUuid(uuid);
|
||||
return this.shopDrawingService.createRevision(sd.id, createRevisionDto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,6 +289,28 @@ export class ShopDrawingService {
|
||||
return shopDrawing;
|
||||
}
|
||||
|
||||
async findOneByUuid(uuid: string) {
|
||||
const shopDrawing = await this.shopDrawingRepo.findOne({
|
||||
where: { uuid },
|
||||
relations: [
|
||||
'mainCategory',
|
||||
'subCategory',
|
||||
'revisions',
|
||||
'revisions.attachments',
|
||||
'revisions.contractDrawings',
|
||||
],
|
||||
order: {
|
||||
revisions: { revisionNumber: 'DESC' },
|
||||
},
|
||||
});
|
||||
|
||||
if (!shopDrawing) {
|
||||
throw new NotFoundException(`Shop Drawing UUID ${uuid} not found`);
|
||||
}
|
||||
|
||||
return shopDrawing;
|
||||
}
|
||||
|
||||
/**
|
||||
* ลบ Shop Drawing
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
PrimaryColumn, // ✅ [Fix] เพิ่ม Import นี้
|
||||
} from 'typeorm';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||
import { Exclude } from 'class-transformer';
|
||||
|
||||
export enum NotificationType {
|
||||
EMAIL = 'EMAIL',
|
||||
@@ -16,8 +18,9 @@ export enum NotificationType {
|
||||
}
|
||||
|
||||
@Entity('notifications')
|
||||
export class Notification {
|
||||
export class Notification extends UuidBaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
@Exclude()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'user_id' })
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Put,
|
||||
Param,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { Controller, Get, Put, Param, UseGuards, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
@@ -17,6 +9,7 @@ import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { SearchNotificationDto } from './dto/search-notification.dto'; // ✅ Import
|
||||
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||
|
||||
@ApiTags('Notifications')
|
||||
@ApiBearerAuth()
|
||||
@@ -26,14 +19,14 @@ export class NotificationController {
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
@InjectRepository(Notification)
|
||||
private notificationRepo: Repository<Notification>,
|
||||
private notificationRepo: Repository<Notification>
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get my notifications' })
|
||||
async getMyNotifications(
|
||||
@CurrentUser() user: User,
|
||||
@Query() searchDto: SearchNotificationDto, // ✅ ใช้ DTO แทน
|
||||
@Query() searchDto: SearchNotificationDto // ✅ ใช้ DTO แทน
|
||||
) {
|
||||
const { page = 1, limit = 20, isRead } = searchDto;
|
||||
|
||||
@@ -65,13 +58,13 @@ export class NotificationController {
|
||||
return { unreadCount: count };
|
||||
}
|
||||
|
||||
@Put(':id/read')
|
||||
@Put(':uuid/read')
|
||||
@ApiOperation({ summary: 'Mark notification as read' })
|
||||
async markAsRead(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@CurrentUser() user: User,
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@CurrentUser() user: User
|
||||
) {
|
||||
return this.notificationService.markAsRead(id, user.user_id);
|
||||
return this.notificationService.markAsReadByUuid(uuid, user.user_id);
|
||||
}
|
||||
|
||||
@Put('read-all')
|
||||
|
||||
@@ -39,7 +39,7 @@ export class NotificationService {
|
||||
@InjectRepository(User)
|
||||
private userRepo: Repository<User>,
|
||||
// ไม่ต้อง Inject UserPrefRepo แล้ว เพราะ Processor จะจัดการเอง
|
||||
private notificationGateway: NotificationGateway,
|
||||
private notificationGateway: NotificationGateway
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -84,14 +84,14 @@ export class NotificationService {
|
||||
delay: 5000,
|
||||
},
|
||||
removeOnComplete: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.logger.debug(`Dispatched notification job for user ${data.userId}`);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to process notification for user ${data.userId}`,
|
||||
(error as Error).stack,
|
||||
(error as Error).stack
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -154,10 +154,25 @@ export class NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
async markAsReadByUuid(uuid: string, userId: number): Promise<void> {
|
||||
const notification = await this.notificationRepo.findOne({
|
||||
where: { uuid, userId },
|
||||
});
|
||||
|
||||
if (!notification) {
|
||||
throw new NotFoundException(`Notification UUID ${uuid} not found`);
|
||||
}
|
||||
|
||||
if (!notification.isRead) {
|
||||
notification.isRead = true;
|
||||
await this.notificationRepo.save(notification);
|
||||
}
|
||||
}
|
||||
|
||||
async markAllAsRead(userId: number): Promise<void> {
|
||||
await this.notificationRepo.update(
|
||||
{ userId, isRead: false },
|
||||
{ isRead: true },
|
||||
{ isRead: true }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,13 @@ import {
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { OrganizationRole } from './organization-role.entity';
|
||||
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||
import { Exclude } from 'class-transformer';
|
||||
|
||||
@Entity('organizations')
|
||||
export class Organization {
|
||||
export class Organization extends UuidBaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
@Exclude()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'organization_code', length: 20, unique: true })
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Delete,
|
||||
Query,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { OrganizationService } from './organization.service.js';
|
||||
@@ -17,6 +16,7 @@ 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';
|
||||
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||
|
||||
@ApiTags('Organizations')
|
||||
@ApiBearerAuth()
|
||||
@@ -38,26 +38,26 @@ export class OrganizationController {
|
||||
return this.orgService.findAll(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get Organization by ID' })
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.orgService.findOne(id);
|
||||
@Get(':uuid')
|
||||
@ApiOperation({ summary: 'Get Organization by UUID' })
|
||||
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||
return this.orgService.findOneByUuid(uuid);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Patch(':uuid')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Update Organization' })
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@Body() dto: UpdateOrganizationDto
|
||||
) {
|
||||
return this.orgService.update(id, dto);
|
||||
return this.orgService.update(uuid, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Delete(':uuid')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Delete Organization' })
|
||||
remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.orgService.remove(id);
|
||||
remove(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||
return this.orgService.remove(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,17 +86,21 @@ export class OrganizationService {
|
||||
return org;
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateOrganizationDto) {
|
||||
const org = await this.findOne(id);
|
||||
async findOneByUuid(uuid: string) {
|
||||
const org = await this.orgRepo.findOne({ where: { uuid } });
|
||||
if (!org)
|
||||
throw new NotFoundException(`Organization UUID ${uuid} not found`);
|
||||
return org;
|
||||
}
|
||||
|
||||
async update(uuid: string, dto: UpdateOrganizationDto) {
|
||||
const org = await this.findOneByUuid(uuid);
|
||||
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.
|
||||
async remove(uuid: string) {
|
||||
const org = await this.findOneByUuid(uuid);
|
||||
return this.orgRepo.remove(org);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,36 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
OneToMany,
|
||||
BeforeInsert,
|
||||
} from 'typeorm';
|
||||
import { v7 as uuidv7 } from 'uuid';
|
||||
import { Exclude } from 'class-transformer';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Contract } from '../../contract/entities/contract.entity';
|
||||
|
||||
@Entity('projects')
|
||||
export class Project extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
@Exclude()
|
||||
id!: number;
|
||||
|
||||
@Column({
|
||||
type: 'uuid',
|
||||
unique: true,
|
||||
nullable: false,
|
||||
comment: 'UUID Public Identifier (ADR-019)',
|
||||
})
|
||||
uuid!: string;
|
||||
|
||||
@BeforeInsert()
|
||||
generateUuid(): void {
|
||||
if (!this.uuid) {
|
||||
this.uuid = uuidv7();
|
||||
}
|
||||
}
|
||||
|
||||
@Column({ name: 'project_code', unique: true, length: 50 })
|
||||
projectCode!: string;
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Delete,
|
||||
Query,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
@@ -19,6 +18,7 @@ import { SearchProjectDto } from './dto/search-project.dto';
|
||||
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
|
||||
@ApiTags('Projects')
|
||||
@@ -49,34 +49,34 @@ export class ProjectController {
|
||||
return this.projectService.findAllOrganizations();
|
||||
}
|
||||
|
||||
@Get(':id/contracts')
|
||||
@Get(':uuid/contracts')
|
||||
@ApiOperation({ summary: 'List All Contracts in Project' })
|
||||
@RequirePermission('project.view')
|
||||
findContracts(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.projectService.findContracts(id);
|
||||
findContracts(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||
return this.projectService.findContracts(uuid);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Get(':uuid')
|
||||
@ApiOperation({ summary: 'Get Project Details' })
|
||||
@RequirePermission('project.view')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.projectService.findOne(id);
|
||||
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||
return this.projectService.findOneByUuid(uuid);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Patch(':uuid')
|
||||
@ApiOperation({ summary: 'Update Project' })
|
||||
@RequirePermission('project.edit')
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@Body() updateDto: UpdateProjectDto
|
||||
) {
|
||||
return this.projectService.update(id, updateDto);
|
||||
return this.projectService.update(uuid, updateDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Delete(':uuid')
|
||||
@ApiOperation({ summary: 'Delete Project (Soft Delete)' })
|
||||
@RequirePermission('project.delete')
|
||||
remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.projectService.remove(id);
|
||||
remove(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||
return this.projectService.remove(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,8 +91,21 @@ export class ProjectService {
|
||||
return project;
|
||||
}
|
||||
|
||||
async update(id: number, updateDto: UpdateProjectDto) {
|
||||
const project = await this.findOne(id);
|
||||
async findOneByUuid(uuid: string) {
|
||||
const project = await this.projectRepository.findOne({
|
||||
where: { uuid },
|
||||
relations: ['contracts'],
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundException(`Project UUID ${uuid} not found`);
|
||||
}
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
async update(uuid: string, updateDto: UpdateProjectDto) {
|
||||
const project = await this.findOneByUuid(uuid);
|
||||
|
||||
// Merge ข้อมูลใหม่ใส่ข้อมูลเดิม
|
||||
this.projectRepository.merge(project, updateDto);
|
||||
@@ -100,22 +113,14 @@ export class ProjectService {
|
||||
return this.projectRepository.save(project);
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
const project = await this.findOne(id);
|
||||
async remove(uuid: string) {
|
||||
const project = await this.findOneByUuid(uuid);
|
||||
// ใช้ Soft Delete
|
||||
return this.projectRepository.softRemove(project);
|
||||
}
|
||||
|
||||
async findContracts(projectId: number) {
|
||||
const project = await this.projectRepository.findOne({
|
||||
where: { id: projectId },
|
||||
relations: ['contracts'],
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundException(`Project ID ${projectId} not found`);
|
||||
}
|
||||
|
||||
async findContracts(uuid: string) {
|
||||
const project = await this.findOneByUuid(uuid);
|
||||
return project.contracts;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,13 @@ import {
|
||||
import { Organization } from '../../organization/entities/organization.entity'; // Adjust path as needed
|
||||
import { UserAssignment } from './user-assignment.entity';
|
||||
import { UserPreference } from './user-preference.entity';
|
||||
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||
import { Exclude } from 'class-transformer';
|
||||
|
||||
@Entity('users')
|
||||
export class User {
|
||||
export class User extends UuidBaseEntity {
|
||||
@PrimaryGeneratedColumn({ name: 'user_id' })
|
||||
@Exclude()
|
||||
user_id!: number;
|
||||
|
||||
@Column({ unique: true, length: 50 })
|
||||
|
||||
@@ -33,6 +33,7 @@ import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||
import { User } from './entities/user.entity';
|
||||
|
||||
@ApiTags('Users')
|
||||
@@ -123,35 +124,35 @@ export class UserController {
|
||||
return this.userService.findAll(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Get(':uuid')
|
||||
@ApiOperation({ summary: 'Get user details' })
|
||||
@ApiParam({ name: 'id', description: 'User ID' })
|
||||
@ApiParam({ name: 'uuid', description: 'User UUID' })
|
||||
@ApiResponse({ status: 200, description: 'User details' })
|
||||
@RequirePermission('user.view')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.userService.findOne(id);
|
||||
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||
return this.userService.findOneByUuid(uuid);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Patch(':uuid')
|
||||
@ApiOperation({ summary: 'Update user' })
|
||||
@ApiParam({ name: 'id', description: 'User ID' })
|
||||
@ApiParam({ name: 'uuid', description: 'User UUID' })
|
||||
@ApiBody({ type: UpdateUserDto })
|
||||
@ApiResponse({ status: 200, description: 'User updated' })
|
||||
@RequirePermission('user.edit')
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@Body() updateUserDto: UpdateUserDto
|
||||
) {
|
||||
return this.userService.update(id, updateUserDto);
|
||||
return this.userService.update(uuid, updateUserDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Delete(':uuid')
|
||||
@ApiOperation({ summary: 'Delete user (Soft delete)' })
|
||||
@ApiParam({ name: 'id', description: 'User ID' })
|
||||
@ApiParam({ name: 'uuid', description: 'User UUID' })
|
||||
@ApiResponse({ status: 200, description: 'User deleted' })
|
||||
@RequirePermission('user.delete')
|
||||
remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.userService.remove(id);
|
||||
remove(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||
return this.userService.remove(uuid);
|
||||
}
|
||||
|
||||
// --- Role Assignment ---
|
||||
|
||||
@@ -133,13 +133,31 @@ export class UserService {
|
||||
return user;
|
||||
}
|
||||
|
||||
async findOneByUuid(uuid: string): Promise<User> {
|
||||
const user = await this.usersRepository.findOne({
|
||||
where: { uuid },
|
||||
relations: [
|
||||
'preference',
|
||||
'assignments',
|
||||
'assignments.role',
|
||||
'assignments.role.permissions',
|
||||
],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with UUID ${uuid} not found`);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async findOneByUsername(username: string): Promise<User | null> {
|
||||
return this.usersRepository.findOne({ where: { username } });
|
||||
}
|
||||
|
||||
// 4. แก้ไขข้อมูล
|
||||
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
|
||||
const user = await this.findOne(id);
|
||||
async update(uuid: string, updateUserDto: UpdateUserDto): Promise<User> {
|
||||
const user = await this.findOneByUuid(uuid);
|
||||
|
||||
if (updateUserDto.password) {
|
||||
const salt = await bcrypt.genSalt();
|
||||
@@ -150,20 +168,21 @@ export class UserService {
|
||||
const savedUser = await this.usersRepository.save(updatedUser);
|
||||
|
||||
// ⚠️ สำคัญ: เมื่อมีการแก้ไขข้อมูล User ต้องเคลียร์ Cache สิทธิ์เสมอ
|
||||
await this.clearUserCache(id);
|
||||
await this.clearUserCache(user.user_id);
|
||||
|
||||
return savedUser;
|
||||
}
|
||||
|
||||
// 5. ลบผู้ใช้ (Soft Delete)
|
||||
async remove(id: number): Promise<void> {
|
||||
const result = await this.usersRepository.softDelete(id);
|
||||
async remove(uuid: string): Promise<void> {
|
||||
const user = await this.findOneByUuid(uuid);
|
||||
const result = await this.usersRepository.softDelete(user.user_id);
|
||||
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`User with ID ${id} not found`);
|
||||
throw new NotFoundException(`User with UUID ${uuid} not found`);
|
||||
}
|
||||
// เคลียร์ Cache เมื่อลบ
|
||||
await this.clearUserCache(id);
|
||||
await this.clearUserCache(user.user_id);
|
||||
}
|
||||
|
||||
async findDocControlIdByOrg(organizationId: number): Promise<number | null> {
|
||||
|
||||
Reference in New Issue
Block a user