Files
lcbp3/lint-results.json
T
admin 11984bfa29
CI Pipeline / build (push) Failing after 12m41s
Build and Deploy / deploy (push) Failing after 2m44s
260322:1648 Correct Coresspondence / Doing RFA / Correct CI
2026-03-22 16:48:12 +07:00

9465 lines
613 KiB
JSON

[
{
"filePath": "D:\\nap-dms.lcbp3\\.agents\\scripts\\start-mcp.js",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\analyze-any.cjs",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\eslint.config.mjs",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\app.controller.spec.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\app.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\app.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\app.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\auth.controller.spec.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\auth.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\auth.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\auth.service.spec.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-unsafe-assignment",
"severity": 2,
"message": "Unsafe assignment of an `any` value.",
"line": 21,
"column": 7,
"nodeType": "VariableDeclarator",
"messageId": "anyAssignment",
"endLine": 21,
"endColumn": 33
},
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'jwtService' is assigned a value but never used. Allowed unused vars must match /^_/u.",
"line": 26,
"column": 7,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 26,
"endColumn": 17
},
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'tokenRepo' is assigned a value but never used. Allowed unused vars must match /^_/u.",
"line": 27,
"column": 7,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 27,
"endColumn": 16
},
{
"ruleId": "@typescript-eslint/no-unsafe-call",
"severity": 2,
"message": "Unsafe call of an `any` typed value.",
"line": 56,
"column": 5,
"nodeType": "MemberExpression",
"messageId": "unsafeCall",
"endLine": 56,
"endColumn": 37
},
{
"ruleId": "@typescript-eslint/no-unsafe-member-access",
"severity": 2,
"message": "Unsafe member access .compare on an `any` value.",
"line": 56,
"column": 12,
"nodeType": "Identifier",
"messageId": "unsafeMemberExpression",
"endLine": 56,
"endColumn": 19
},
{
"ruleId": "@typescript-eslint/no-unsafe-call",
"severity": 2,
"message": "Unsafe call of an `any` typed value.",
"line": 131,
"column": 7,
"nodeType": "MemberExpression",
"messageId": "unsafeCall",
"endLine": 131,
"endColumn": 43
},
{
"ruleId": "@typescript-eslint/no-unsafe-member-access",
"severity": 2,
"message": "Unsafe member access .compare on an `any` value.",
"line": 131,
"column": 14,
"nodeType": "Identifier",
"messageId": "unsafeMemberExpression",
"endLine": 131,
"endColumn": 21
},
{
"ruleId": "@typescript-eslint/unbound-method",
"severity": 2,
"message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.",
"line": 165,
"column": 14,
"nodeType": "MemberExpression",
"messageId": "unboundWithoutThisAnnotation",
"endLine": 165,
"endColumn": 32
}
],
"suppressedMessages": [
{
"ruleId": "@typescript-eslint/no-require-imports",
"severity": 2,
"message": "A `require()` style import is forbidden.",
"line": 21,
"column": 16,
"nodeType": "CallExpression",
"messageId": "noRequireImports",
"endLine": 21,
"endColumn": 33,
"suppressions": [{ "kind": "directive", "justification": "" }]
}
],
"errorCount": 8,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import { Test, TestingModule } from '@nestjs/testing';\r\nimport { AuthService } from './auth.service';\r\nimport { UserService } from '../../modules/user/user.service';\r\nimport { JwtService } from '@nestjs/jwt';\r\nimport { ConfigService } from '@nestjs/config';\r\nimport { CACHE_MANAGER } from '@nestjs/cache-manager';\r\nimport { getRepositoryToken } from '@nestjs/typeorm';\r\nimport { User } from '../../modules/user/entities/user.entity';\r\nimport { RefreshToken } from './entities/refresh-token.entity';\r\nimport { Repository } from 'typeorm';\r\nimport { UnauthorizedException } from '@nestjs/common';\r\n\r\n// Mock bcrypt at top level\r\njest.mock('bcrypt', () => ({\r\n compare: jest.fn(),\r\n hash: jest.fn().mockResolvedValue('hashedpassword'),\r\n genSalt: jest.fn().mockResolvedValue('salt'),\r\n}));\r\n\r\n// eslint-disable-next-line @typescript-eslint/no-require-imports\r\nconst bcrypt = require('bcrypt');\r\n\r\ndescribe('AuthService', () => {\r\n let service: AuthService;\r\n let userService: UserService;\r\n let jwtService: JwtService;\r\n let tokenRepo: Repository<RefreshToken>;\r\n\r\n const mockUser = {\r\n user_id: 1,\r\n username: 'testuser',\r\n password: 'hashedpassword',\r\n primaryOrganizationId: 1,\r\n };\r\n\r\n const mockQueryBuilder = {\r\n addSelect: jest.fn().mockReturnThis(),\r\n leftJoinAndSelect: jest.fn().mockReturnThis(),\r\n where: jest.fn().mockReturnThis(),\r\n getOne: jest.fn().mockResolvedValue(mockUser),\r\n };\r\n\r\n const mockUserRepo = {\r\n createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder),\r\n };\r\n\r\n const mockTokenRepo = {\r\n create: jest.fn(),\r\n save: jest.fn(),\r\n findOne: jest.fn(),\r\n update: jest.fn(),\r\n };\r\n\r\n beforeEach(async () => {\r\n // Reset bcrypt mocks\r\n bcrypt.compare.mockResolvedValue(true);\r\n\r\n const module: TestingModule = await Test.createTestingModule({\r\n providers: [\r\n AuthService,\r\n {\r\n provide: UserService,\r\n useValue: {\r\n findOneByUsername: jest.fn(),\r\n create: jest.fn(),\r\n findOne: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: JwtService,\r\n useValue: {\r\n signAsync: jest.fn().mockResolvedValue('jwt_token'),\r\n decode: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: ConfigService,\r\n useValue: {\r\n get: jest.fn().mockImplementation((key: string) => {\r\n if (key.includes('EXPIRATION')) return '1h';\r\n return 'secret';\r\n }),\r\n },\r\n },\r\n {\r\n provide: CACHE_MANAGER,\r\n useValue: {\r\n set: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: getRepositoryToken(User),\r\n useValue: mockUserRepo,\r\n },\r\n {\r\n provide: getRepositoryToken(RefreshToken),\r\n useValue: mockTokenRepo,\r\n },\r\n ],\r\n }).compile();\r\n\r\n service = module.get<AuthService>(AuthService);\r\n userService = module.get<UserService>(UserService);\r\n jwtService = module.get<JwtService>(JwtService);\r\n tokenRepo = module.get(getRepositoryToken(RefreshToken));\r\n });\r\n\r\n afterEach(() => {\r\n jest.clearAllMocks();\r\n });\r\n\r\n it('should be defined', () => {\r\n expect(service).toBeDefined();\r\n });\r\n\r\n describe('validateUser', () => {\r\n it('should return user without password if validation succeeds', async () => {\r\n const result = await service.validateUser('testuser', 'password');\r\n expect(result).toBeDefined();\r\n expect(result).not.toHaveProperty('password');\r\n expect(result.username).toBe('testuser');\r\n });\r\n\r\n it('should return null if user not found', async () => {\r\n mockQueryBuilder.getOne.mockResolvedValueOnce(null);\r\n const result = await service.validateUser('unknown', 'password');\r\n expect(result).toBeNull();\r\n });\r\n\r\n it('should return null if password mismatch', async () => {\r\n bcrypt.compare.mockResolvedValueOnce(false);\r\n const result = await service.validateUser('testuser', 'wrongpassword');\r\n expect(result).toBeNull();\r\n });\r\n });\r\n\r\n describe('login', () => {\r\n it('should return access and refresh tokens', async () => {\r\n mockTokenRepo.create.mockReturnValue({ id: 1 });\r\n mockTokenRepo.save.mockResolvedValue({ id: 1 });\r\n\r\n const result = await service.login(mockUser);\r\n\r\n expect(result).toHaveProperty('access_token');\r\n expect(result).toHaveProperty('refresh_token');\r\n expect(mockTokenRepo.save).toHaveBeenCalled();\r\n });\r\n });\r\n\r\n describe('register', () => {\r\n it('should register a new user', async () => {\r\n (userService.findOneByUsername as jest.Mock).mockResolvedValue(null);\r\n (userService.create as jest.Mock).mockResolvedValue(mockUser);\r\n\r\n const dto = {\r\n username: 'newuser',\r\n password: 'password',\r\n email: 'test@example.com',\r\n firstName: 'Test',\r\n lastName: 'User',\r\n };\r\n\r\n const result = await service.register(dto);\r\n expect(result).toBeDefined();\r\n expect(userService.create).toHaveBeenCalled();\r\n });\r\n });\r\n\r\n describe('refreshToken', () => {\r\n it('should return new tokens if valid', async () => {\r\n const mockStoredToken = {\r\n tokenHash: 'somehash',\r\n isRevoked: false,\r\n expiresAt: new Date(Date.now() + 10000),\r\n };\r\n mockTokenRepo.findOne.mockResolvedValue(mockStoredToken);\r\n (userService.findOne as jest.Mock).mockResolvedValue(mockUser);\r\n\r\n const result = await service.refreshToken(1, 'valid_refresh_token');\r\n\r\n expect(result.access_token).toBeDefined();\r\n expect(result.refresh_token).toBeDefined();\r\n // Should mark old token as revoked\r\n expect(mockTokenRepo.save).toHaveBeenCalledWith(\r\n expect.objectContaining({ isRevoked: true })\r\n );\r\n });\r\n\r\n it('should throw UnauthorizedException if token revoked', async () => {\r\n const mockStoredToken = {\r\n tokenHash: 'somehash',\r\n isRevoked: true,\r\n expiresAt: new Date(Date.now() + 10000),\r\n };\r\n mockTokenRepo.findOne.mockResolvedValue(mockStoredToken);\r\n\r\n await expect(service.refreshToken(1, 'revoked_token')).rejects.toThrow(\r\n UnauthorizedException\r\n );\r\n });\r\n\r\n it('should allow refresh within 30s grace period if already revoked', async () => {\r\n const updatedAt = new Date(Date.now() - 5000); // 5 seconds ago\r\n const mockStoredToken = {\r\n tokenHash: 'somehash',\r\n isRevoked: true,\r\n updatedAt: updatedAt,\r\n replacedByToken: 'new_token_hash',\r\n expiresAt: new Date(Date.now() + 10000),\r\n };\r\n mockTokenRepo.findOne.mockResolvedValue(mockStoredToken);\r\n (userService.findOne as jest.Mock).mockResolvedValue(mockUser);\r\n mockTokenRepo.create.mockReturnValue({ token_id: 2 });\r\n mockTokenRepo.save.mockResolvedValue({ token_id: 2 });\r\n\r\n const result = await service.refreshToken(1, 'valid_refresh_token');\r\n\r\n expect(result.access_token).toBeDefined();\r\n expect(result.refresh_token).toBeDefined();\r\n // Should not call revokeAllUserTokens\r\n expect(mockTokenRepo.update).not.toHaveBeenCalled();\r\n });\r\n\r\n it('should throw UnauthorizedException if token revoked more than 30s ago', async () => {\r\n const updatedAt = new Date(Date.now() - 35000); // 35 seconds ago\r\n const mockStoredToken = {\r\n tokenHash: 'somehash',\r\n isRevoked: true,\r\n updatedAt: updatedAt,\r\n replacedByToken: 'new_token_hash',\r\n expiresAt: new Date(Date.now() + 10000),\r\n };\r\n mockTokenRepo.findOne.mockResolvedValue(mockStoredToken);\r\n\r\n await expect(service.refreshToken(1, 'revoked_token')).rejects.toThrow(\r\n UnauthorizedException\r\n );\r\n });\r\n });\r\n});\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\auth.service.ts",
"messages": [],
"suppressedMessages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'password' is assigned a value but never used. Allowed unused vars must match /^_/u.",
"line": 82,
"column": 15,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 82,
"endColumn": 23,
"suppressions": [{ "kind": "directive", "justification": "" }]
}
],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\casl\\ability.factory.spec.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\casl\\ability.factory.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\casl\\casl.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\dto\\login.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\dto\\register.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\entities\\refresh-token.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\guards\\permissions.guard.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\session.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\strategies\\jwt-refresh.strategy.ts",
"messages": [
{
"ruleId": "@typescript-eslint/require-await",
"severity": 2,
"message": "Async method 'validate' has no 'await' expression.",
"line": 27,
"column": 3,
"nodeType": "FunctionExpression",
"messageId": "missingAwait",
"endLine": 27,
"endColumn": 17,
"suggestions": [
{ "messageId": "removeAsync", "fix": { "range": [960, 966], "text": "" }, "desc": "Remove 'async'." }
]
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "// File: src/common/auth/strategies/jwt-refresh.strategy.ts\r\n// บันทึกการแก้ไข: Strategy สำหรับ Refresh Token (T1.2)\r\n// บันทึกการแก้ไข: แก้ไข TS2345 โดยยืนยันค่า secretOrKey ด้วย ! (Non-null assertion)\r\n\r\nimport { ExtractJwt, Strategy } from 'passport-jwt';\r\nimport { PassportStrategy } from '@nestjs/passport';\r\nimport { Injectable } from '@nestjs/common';\r\nimport { ConfigService } from '@nestjs/config';\r\nimport { Request } from 'express';\r\nimport type { JwtPayload } from './jwt.strategy';\r\n\r\n@Injectable()\r\nexport class JwtRefreshStrategy extends PassportStrategy(\r\n Strategy,\r\n 'jwt-refresh'\r\n) {\r\n constructor(configService: ConfigService) {\r\n super({\r\n jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),\r\n ignoreExpiration: false,\r\n // ✅ Fix: ใส่ ! เพื่อบอก TS ว่าค่านี้มีอยู่จริง (จาก env validation)\r\n secretOrKey: configService.get<string>('JWT_REFRESH_SECRET')!,\r\n passReqToCallback: true,\r\n });\r\n }\r\n\r\n async validate(req: Request, payload: JwtPayload) {\r\n const refreshToken = ExtractJwt.fromAuthHeaderAsBearerToken()(req);\r\n return {\r\n ...payload,\r\n refreshToken,\r\n };\r\n }\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\auth\\strategies\\jwt.strategy.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\common.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\config\\env.validation.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\config\\redis.config.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\decorators\\audit.decorator.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\decorators\\bypass-maintenance.decorator.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\decorators\\circuit-breaker.decorator.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\decorators\\current-user.decorator.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\decorators\\idempotency.decorator.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\decorators\\require-permission.decorator.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\decorators\\retry.decorator.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\entities\\audit-log.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\entities\\base.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\entities\\uuid-base.entity.spec.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\entities\\uuid-base.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\exceptions\\http-exception.filter.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\file-storage\\entities\\attachment.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\file-storage\\file-cleanup.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\file-storage\\file-storage.controller.spec.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\file-storage\\file-storage.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\file-storage\\file-storage.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\file-storage\\file-storage.service.spec.ts",
"messages": [
{
"ruleId": "@typescript-eslint/unbound-method",
"severity": 2,
"message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.",
"line": 88,
"column": 14,
"nodeType": "MemberExpression",
"messageId": "unboundWithoutThisAnnotation",
"endLine": 88,
"endColumn": 35
},
{
"ruleId": "@typescript-eslint/unbound-method",
"severity": 2,
"message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.",
"line": 89,
"column": 14,
"nodeType": "MemberExpression",
"messageId": "unboundWithoutThisAnnotation",
"endLine": 89,
"endColumn": 33
},
{
"ruleId": "@typescript-eslint/unbound-method",
"severity": 2,
"message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.",
"line": 121,
"column": 14,
"nodeType": "MemberExpression",
"messageId": "unboundWithoutThisAnnotation",
"endLine": 121,
"endColumn": 33
},
{
"ruleId": "@typescript-eslint/unbound-method",
"severity": 2,
"message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.",
"line": 139,
"column": 14,
"nodeType": "MemberExpression",
"messageId": "unboundWithoutThisAnnotation",
"endLine": 139,
"endColumn": 35
}
],
"suppressedMessages": [],
"errorCount": 4,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import { Test, TestingModule } from '@nestjs/testing';\r\nimport { FileStorageService } from './file-storage.service';\r\nimport { getRepositoryToken } from '@nestjs/typeorm';\r\nimport { Attachment } from './entities/attachment.entity';\r\nimport { ConfigService } from '@nestjs/config';\r\nimport * as fs from 'fs-extra';\r\nimport {\r\n BadRequestException,\r\n NotFoundException,\r\n ForbiddenException,\r\n} from '@nestjs/common';\r\nimport { Repository } from 'typeorm';\r\n\r\n// Mock fs-extra\r\njest.mock('fs-extra');\r\n\r\ndescribe('FileStorageService', () => {\r\n let service: FileStorageService;\r\n let attachmentRepo: Repository<Attachment>;\r\n\r\n const mockAttachment = {\r\n id: 1,\r\n originalFilename: 'test.pdf',\r\n storedFilename: 'uuid.pdf',\r\n filePath: '/permanent/2024/12/uuid.pdf',\r\n fileSize: 1024,\r\n uploadedByUserId: 1,\r\n } as Attachment;\r\n\r\n const mockFile = {\r\n originalname: 'test.pdf',\r\n mimetype: 'application/pdf',\r\n size: 1024,\r\n buffer: Buffer.from('test-content'),\r\n } as Express.Multer.File;\r\n\r\n beforeEach(async () => {\r\n const module: TestingModule = await Test.createTestingModule({\r\n providers: [\r\n FileStorageService,\r\n {\r\n provide: getRepositoryToken(Attachment),\r\n useValue: {\r\n create: jest.fn().mockReturnValue(mockAttachment),\r\n save: jest.fn().mockResolvedValue(mockAttachment),\r\n find: jest.fn(),\r\n findOne: jest.fn(),\r\n remove: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: ConfigService,\r\n useValue: {\r\n get: jest.fn((key) => {\r\n if (key === 'NODE_ENV') return 'test';\r\n return null;\r\n }),\r\n },\r\n },\r\n ],\r\n }).compile();\r\n\r\n service = module.get<FileStorageService>(FileStorageService);\r\n attachmentRepo = module.get(getRepositoryToken(Attachment));\r\n\r\n jest.clearAllMocks();\r\n (fs.ensureDirSync as unknown as jest.Mock).mockReturnValue(true);\r\n (fs.writeFile as unknown as jest.Mock).mockResolvedValue(undefined);\r\n (fs.pathExists as unknown as jest.Mock).mockResolvedValue(true);\r\n (fs.move as unknown as jest.Mock).mockResolvedValue(undefined);\r\n (fs.remove as unknown as jest.Mock).mockResolvedValue(undefined);\r\n (fs.readFile as unknown as jest.Mock).mockResolvedValue(\r\n Buffer.from('test')\r\n );\r\n (fs.stat as unknown as jest.Mock).mockResolvedValue({ size: 1024 });\r\n (fs.ensureDir as unknown as jest.Mock).mockResolvedValue(undefined);\r\n });\r\n\r\n it('should be defined', () => {\r\n expect(service).toBeDefined();\r\n });\r\n\r\n describe('upload', () => {\r\n it('should save file to temp and create DB record', async () => {\r\n const result = await service.upload(mockFile, 1);\r\n\r\n expect(fs.writeFile).toHaveBeenCalled();\r\n expect(attachmentRepo.create).toHaveBeenCalled();\r\n expect(attachmentRepo.save).toHaveBeenCalled();\r\n expect(result).toBeDefined();\r\n });\r\n\r\n it('should throw BadRequestException if write fails', async () => {\r\n (fs.writeFile as unknown as jest.Mock).mockRejectedValueOnce(\r\n new Error('Write error')\r\n );\r\n await expect(service.upload(mockFile, 1)).rejects.toThrow(\r\n BadRequestException\r\n );\r\n });\r\n });\r\n\r\n describe('commit', () => {\r\n it('should move files to permanent storage', async () => {\r\n const tempIds = ['uuid-1'];\r\n const mockAttachments = [\r\n {\r\n ...mockAttachment,\r\n isTemporary: true,\r\n tempId: 'uuid-1',\r\n filePath: '/temp/uuid.pdf',\r\n },\r\n ];\r\n\r\n (attachmentRepo.find as jest.Mock).mockResolvedValue(mockAttachments);\r\n\r\n await service.commit(tempIds);\r\n\r\n expect(fs.ensureDir).toHaveBeenCalled();\r\n expect(fs.move).toHaveBeenCalled();\r\n expect(attachmentRepo.save).toHaveBeenCalled();\r\n });\r\n\r\n it('should show warning if file counts mismatch', async () => {\r\n (attachmentRepo.find as jest.Mock).mockResolvedValue([]);\r\n await expect(service.commit(['uuid-1'])).rejects.toThrow(\r\n NotFoundException\r\n );\r\n });\r\n });\r\n\r\n describe('delete', () => {\r\n it('should delete file if user owns it', async () => {\r\n (attachmentRepo.findOne as jest.Mock).mockResolvedValue(mockAttachment);\r\n\r\n await service.delete(1, 1);\r\n\r\n expect(fs.remove).toHaveBeenCalled();\r\n expect(attachmentRepo.remove).toHaveBeenCalled();\r\n });\r\n\r\n it('should throw ForbiddenException if user does not own file', async () => {\r\n (attachmentRepo.findOne as jest.Mock).mockResolvedValue(mockAttachment);\r\n await expect(service.delete(1, 999)).rejects.toThrow(ForbiddenException);\r\n });\r\n });\r\n});\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\file-storage\\file-storage.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\guards\\jwt-auth.guard.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\guards\\jwt-refresh.guard.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\guards\\maintenance-mode.guard.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\guards\\rbac.guard.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\interceptors\\audit-log.interceptor.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\interceptors\\idempotency.interceptor.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\interceptors\\performance.interceptor.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\interceptors\\transform.interceptor.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\interfaces\\request-with-user.interface.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\pipes\\parse-uuid.pipe.spec.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\pipes\\parse-uuid.pipe.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\resilience\\resilience.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\services\\crypto.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\services\\request-context.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\services\\uuid-resolver.service.spec.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\services\\uuid-resolver.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\common\\utils\\uuid-guard.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\config\\database.config.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\database\\migrations\\1701676800000-V1_5_1_Schema_Update.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\database\\migrations\\InitialSchema.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\database\\seeds\\organization.seed.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\database\\seeds\\run-seed.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-unsafe-argument",
"severity": 2,
"message": "Unsafe argument of type `any` assigned to a parameter of type `DataSourceOptions`.",
"line": 7,
"column": 37,
"nodeType": "TSAsExpression",
"messageId": "unsafeArgument",
"endLine": 7,
"endColumn": 58
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 7,
"column": 55,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 7,
"endColumn": 58,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [288, 291], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [288, 291], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 11,
"column": 5,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 11,
"endColumn": 16,
"suggestions": [
{
"fix": { "range": [344, 382], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "log" },
"desc": "Remove the console.log()."
}
]
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 16,
"column": 5,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 16,
"endColumn": 16,
"suggestions": [
{
"fix": { "range": [468, 504], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "log" },
"desc": "Remove the console.log()."
}
]
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 18,
"column": 5,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 18,
"endColumn": 18,
"suggestions": [
{
"fix": { "range": [531, 573], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "error" },
"desc": "Remove the console.error()."
}
]
},
{
"ruleId": "@typescript-eslint/no-floating-promises",
"severity": 2,
"message": "Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.",
"line": 24,
"column": 1,
"nodeType": "ExpressionStatement",
"messageId": "floatingVoid",
"endLine": 24,
"endColumn": 12,
"suggestions": [
{
"messageId": "floatingFixVoid",
"fix": { "range": [633, 633], "text": "void " },
"desc": "Add void operator to ignore."
},
{
"messageId": "floatingFixAwait",
"fix": { "range": [633, 633], "text": "await " },
"desc": "Add await operator."
}
]
}
],
"suppressedMessages": [],
"errorCount": 6,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import { DataSource } from 'typeorm';\r\nimport { databaseConfig } from '../../config/database.config';\r\nimport { seedOrganizations } from './organization.seed';\r\nimport { seedUsers } from './user.seed';\r\n\r\nasync function runSeeds() {\r\n const dataSource = new DataSource(databaseConfig as any);\r\n await dataSource.initialize();\r\n\r\n try {\r\n console.log('🌱 Seeding database...');\r\n\r\n await seedOrganizations(dataSource);\r\n await seedUsers(dataSource);\r\n\r\n console.log('✅ Seeding completed!');\r\n } catch (error) {\r\n console.error('❌ Seeding failed:', error);\r\n } finally {\r\n await dataSource.destroy();\r\n }\r\n}\r\n\r\nrunSeeds();\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\database\\seeds\\user.seed.ts",
"messages": [
{
"ruleId": "@typescript-eslint/ban-ts-comment",
"severity": 2,
"message": "Use \"@ts-expect-error\" instead of \"@ts-ignore\", as \"@ts-ignore\" will do nothing if the following line is error-free.",
"line": 61,
"column": 7,
"nodeType": "Line",
"messageId": "tsIgnoreInsteadOfExpectError",
"endLine": 61,
"endColumn": 20,
"suggestions": [
{
"messageId": "replaceTsIgnoreWithTsExpectError",
"fix": { "range": [2088, 2101], "text": "// @ts-expect-error" },
"desc": "Replace \"@ts-ignore\" with \"@ts-expect-error\"."
}
]
},
{
"ruleId": "@typescript-eslint/no-unsafe-assignment",
"severity": 2,
"message": "Unsafe assignment of an `any` value.",
"line": 115,
"column": 13,
"nodeType": "VariableDeclarator",
"messageId": "anyAssignment",
"endLine": 115,
"endColumn": 43
},
{
"ruleId": "@typescript-eslint/no-unsafe-assignment",
"severity": 2,
"message": "Unsafe assignment of an `any` value.",
"line": 119,
"column": 11,
"nodeType": "Property",
"messageId": "anyAssignment",
"endLine": 119,
"endColumn": 15
}
],
"suppressedMessages": [],
"errorCount": 3,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import { DataSource } from 'typeorm';\r\nimport { User } from '../../modules/user/entities/user.entity';\r\nimport { Role, RoleScope } from '../../modules/user/entities/role.entity';\r\nimport { UserAssignment } from '../../modules/user/entities/user-assignment.entity';\r\nimport * as bcrypt from 'bcrypt';\r\n\r\nexport async function seedUsers(dataSource: DataSource) {\r\n const userRepo = dataSource.getRepository(User);\r\n const roleRepo = dataSource.getRepository(Role);\r\n const assignmentRepo = dataSource.getRepository(UserAssignment);\r\n\r\n // Create Roles\r\n const rolesData = [\r\n {\r\n roleName: 'Superadmin',\r\n scope: RoleScope.GLOBAL,\r\n description:\r\n 'ผู้ดูแลระบบสูงสุด: สามารถทำทุกอย่างในระบบ, จัดการองค์กร, และจัดการข้อมูลหลักระดับ Global',\r\n },\r\n {\r\n roleName: 'Org Admin',\r\n scope: RoleScope.ORGANIZATION,\r\n description:\r\n 'ผู้ดูแลองค์กร: จัดการผู้ใช้ในองค์กร, จัดการบทบาท / สิทธิ์ภายในองค์กร, และดูรายงานขององค์กร',\r\n },\r\n {\r\n roleName: 'Document Control',\r\n scope: RoleScope.ORGANIZATION,\r\n description:\r\n 'ควบคุมเอกสารขององค์กร: เพิ่ม / แก้ไข / ลบเอกสาร, และกำหนดสิทธิ์เอกสารภายในองค์กร',\r\n },\r\n {\r\n roleName: 'Editor',\r\n scope: RoleScope.PROJECT,\r\n description:\r\n 'ผู้แก้ไขเอกสารขององค์กร: เพิ่ม / แก้ไขเอกสารที่ได้รับมอบหมาย',\r\n },\r\n {\r\n roleName: 'Viewer',\r\n scope: RoleScope.PROJECT,\r\n description: 'ผู้ดูเอกสารขององค์กร: ดูเอกสารที่มีสิทธิ์เข้าถึงเท่านั้น',\r\n },\r\n {\r\n roleName: 'Project Manager',\r\n scope: RoleScope.PROJECT,\r\n description:\r\n 'ผู้จัดการโครงการ: จัดการสมาชิกในโครงการ, สร้าง / จัดการสัญญาในโครงการ, และดูรายงานโครงการ',\r\n },\r\n {\r\n roleName: 'Contract Admin',\r\n scope: RoleScope.CONTRACT,\r\n description:\r\n 'ผู้ดูแลสัญญา: จัดการสมาชิกในสัญญา, สร้าง / จัดการข้อมูลหลักเฉพาะสัญญา, และอนุมัติเอกสารในสัญญา',\r\n },\r\n ];\r\n\r\n const roleMap = new Map();\r\n for (const r of rolesData) {\r\n let role = await roleRepo.findOneBy({ roleName: r.roleName });\r\n if (!role) {\r\n // @ts-ignore\r\n role = await roleRepo.save(roleRepo.create(r));\r\n }\r\n roleMap.set(r.roleName, role);\r\n }\r\n\r\n // Create Users\r\n const usersData = [\r\n {\r\n username: 'superadmin',\r\n email: 'superadmin@example.com',\r\n firstName: 'Super',\r\n lastName: 'Admin',\r\n roleName: 'Superadmin',\r\n },\r\n {\r\n username: 'admin',\r\n email: 'admin@example.com',\r\n firstName: 'Admin',\r\n lastName: 'คคง.',\r\n roleName: 'Org Admin',\r\n },\r\n {\r\n username: 'editor01',\r\n email: 'editor01@example.com',\r\n firstName: 'DC',\r\n lastName: 'C1',\r\n roleName: 'Editor',\r\n },\r\n {\r\n username: 'viewer01',\r\n email: 'viewer01@example.com',\r\n firstName: 'Viewer',\r\n lastName: 'สคฉ.03',\r\n roleName: 'Viewer',\r\n },\r\n ];\r\n\r\n const salt = await bcrypt.genSalt();\r\n const password = await bcrypt.hash('password123', salt); // Default password\r\n\r\n for (const u of usersData) {\r\n let user = await userRepo.findOneBy({ username: u.username });\r\n if (!user) {\r\n user = userRepo.create({\r\n username: u.username,\r\n email: u.email,\r\n firstName: u.firstName,\r\n lastName: u.lastName,\r\n password, // Fixed: password instead of passwordHash\r\n });\r\n user = await userRepo.save(user);\r\n\r\n // Create Assignment\r\n const role = roleMap.get(u.roleName);\r\n if (role) {\r\n const assignment = assignmentRepo.create({\r\n user,\r\n role,\r\n assignedAt: new Date(),\r\n });\r\n await assignmentRepo.save(assignment);\r\n }\r\n }\r\n }\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\database\\seeds\\workflow-definitions.seed.ts",
"messages": [
{
"ruleId": "prettier/prettier",
"severity": 2,
"message": "Insert `········`",
"line": 133,
"column": 1,
"nodeType": null,
"messageId": "insert",
"endLine": 133,
"endColumn": 1,
"fix": { "range": [4038, 4038], "text": " " }
},
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'error' is defined but never used.",
"line": 134,
"column": 16,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 134,
"endColumn": 21
},
{
"ruleId": "prettier/prettier",
"severity": 2,
"message": "Insert `········`",
"line": 135,
"column": 1,
"nodeType": null,
"messageId": "insert",
"endLine": 135,
"endColumn": 1,
"fix": { "range": [4142, 4142], "text": " " }
},
{
"ruleId": "prettier/prettier",
"severity": 2,
"message": "Insert `······`",
"line": 138,
"column": 1,
"nodeType": null,
"messageId": "insert",
"endLine": 138,
"endColumn": 1,
"fix": { "range": [4245, 4245], "text": " " }
},
{
"ruleId": "prettier/prettier",
"severity": 2,
"message": "Insert `······`",
"line": 139,
"column": 1,
"nodeType": null,
"messageId": "insert",
"endLine": 139,
"endColumn": 1,
"fix": { "range": [4268, 4268], "text": " " }
},
{
"ruleId": "prettier/prettier",
"severity": 2,
"message": "Insert `······`",
"line": 140,
"column": 1,
"nodeType": null,
"messageId": "insert",
"endLine": 140,
"endColumn": 1,
"fix": { "range": [4343, 4343], "text": " " }
}
],
"suppressedMessages": [],
"errorCount": 6,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 5,
"fixableWarningCount": 0,
"source": "// src/database/seeds/workflow-definitions.seed.ts\r\n\r\nimport { DataSource } from 'typeorm';\r\nimport { WorkflowDefinition } from '../../modules/workflow-engine/entities/workflow-definition.entity';\r\nimport { WorkflowDslService } from '../../modules/workflow-engine/workflow-dsl.service';\r\n\r\nexport const seedWorkflowDefinitions = async (dataSource: DataSource) => {\r\n const repo = dataSource.getRepository(WorkflowDefinition);\r\n const dslService = new WorkflowDslService();\r\n\r\n // 1. RFA Workflow (Standard)\r\n const rfaDsl = {\r\n workflow: 'RFA_FLOW_V1', // [FIX] เปลี่ยนชื่อให้ตรงกับค่าใน RfaWorkflowService\r\n version: 1,\r\n description: 'Standard RFA Approval Workflow',\r\n states: [\r\n {\r\n name: 'DRAFT',\r\n initial: true,\r\n on: {\r\n SUBMIT: {\r\n to: 'IN_REVIEW',\r\n require: { role: 'Editor' }, // [FIX] แก้ไข Syntax เป็น Object\r\n },\r\n },\r\n },\r\n {\r\n name: 'IN_REVIEW',\r\n on: {\r\n APPROVE_1: {\r\n to: 'APPROVED', // [FIX] ชี้ไปที่ State ที่มีอยู่จริง\r\n require: { role: 'Contract Admin' },\r\n condition: \"context.priority === 'HIGH'\",\r\n },\r\n APPROVE_2: {\r\n to: 'APPROVED', // [FIX] ชี้ไปที่ State ที่มีอยู่จริง\r\n require: { role: 'Contract Admin' },\r\n condition: \"context.priority === 'NORMAL'\",\r\n },\r\n REJECT: {\r\n to: 'REJECTED',\r\n require: { role: 'Contract Admin' },\r\n },\r\n COMMENT: {\r\n to: 'DRAFT',\r\n require: { role: 'Contract Admin' },\r\n }, // ส่งกลับแก้ไข\r\n },\r\n },\r\n { name: 'APPROVED', terminal: true },\r\n { name: 'REJECTED', terminal: true },\r\n ],\r\n };\r\n\r\n // 2. Circulation Workflow\r\n const circulationDsl = {\r\n workflow: 'CIRCULATION_INTERNAL_V1', // [FIX] เปลี่ยนชื่อให้ตรงกับค่าใน CirculationWorkflowService\r\n version: 1,\r\n description: 'Internal Document Circulation',\r\n states: [\r\n {\r\n name: 'OPEN',\r\n initial: true,\r\n on: {\r\n START: {\r\n // [FIX] เปลี่ยนชื่อ Action ให้ตรงกับที่ Service เรียกใช้ ('START')\r\n to: 'IN_REVIEW',\r\n },\r\n },\r\n },\r\n {\r\n name: 'IN_REVIEW',\r\n on: {\r\n COMPLETE_TASK: {\r\n // [FIX] เปลี่ยนให้สอดคล้องกับ Action ที่ใช้จริง\r\n to: 'COMPLETED',\r\n },\r\n CANCEL: { to: 'CANCELLED' },\r\n },\r\n },\r\n { name: 'COMPLETED', terminal: true },\r\n { name: 'CANCELLED', terminal: true },\r\n ],\r\n };\r\n\r\n // 3. Correspondence Workflow (Optional - ถ้ามี)\r\n const correspondenceDsl = {\r\n workflow: 'CORRESPONDENCE_FLOW_V1',\r\n version: 1,\r\n description: 'Standard Correspondence Routing',\r\n states: [\r\n {\r\n name: 'DRAFT',\r\n initial: true,\r\n on: { SUBMIT: { to: 'IN_REVIEW' } },\r\n },\r\n {\r\n name: 'IN_REVIEW',\r\n on: {\r\n APPROVE: { to: 'APPROVED' },\r\n REJECT: { to: 'REJECTED' },\r\n },\r\n },\r\n { name: 'APPROVED', terminal: true },\r\n { name: 'REJECTED', terminal: true },\r\n ],\r\n };\r\n\r\n const workflows = [rfaDsl, circulationDsl, correspondenceDsl];\r\n\r\n for (const dsl of workflows) {\r\n const exists = await repo.findOne({\r\n where: { workflow_code: dsl.workflow, version: dsl.version },\r\n });\r\n\r\n if (!exists) {\r\n try {\r\n // Compile เพื่อ Validate และ Normalize ก่อนบันทึก\r\n const compiled = dslService.compile(\r\n dsl as unknown as import('../../modules/workflow-engine/workflow-dsl.service').RawWorkflowDSL\r\n );\r\n\r\n await repo.save(\r\n repo.create({\r\n workflow_code: dsl.workflow,\r\n version: dsl.version,\r\n description: dsl.description,\r\n dsl: dsl as unknown as Record<string, unknown>,\r\n compiled: compiled as unknown as Record<string, unknown>,\r\n is_active: true,\r\n })\r\n );\r\n// console.log(`✅ Seeded Workflow: ${dsl.workflow} v${dsl.version}`);\r\n } catch (error) {\r\n// console.error(`❌ Failed to seed workflow ${dsl.workflow}:`, error);\r\n }\r\n } else {\r\n// console.log(\r\n// `⏭️ Workflow already exists: ${dsl.workflow} v${dsl.version}`\r\n// );\r\n }\r\n }\r\n};\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\main.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\audit-log\\audit-log.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\audit-log\\audit-log.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\audit-log\\audit-log.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\auth\\entities\\role.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\circulation-workflow.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\circulation.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\circulation.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\circulation.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\dto\\create-circulation.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\dto\\search-circulation.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\dto\\update-circulation-routing.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\entities\\circulation-routing.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\entities\\circulation-status-code.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\circulation\\entities\\circulation.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\contract\\contract.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\contract\\contract.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\contract\\contract.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\contract\\dto\\create-contract.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\contract\\dto\\search-contract.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\contract\\dto\\update-contract.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\contract\\entities\\contract-organization.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\contract\\entities\\contract.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\correspondence-workflow.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\correspondence.controller.spec.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\correspondence.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\correspondence.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\correspondence.service.spec.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'CorrespondenceRecipient' is defined but never used. Allowed unused vars must match /^_/u.",
"line": 11,
"column": 10,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 11,
"endColumn": 33,
"suggestions": [
{
"messageId": "removeUnusedImportDeclaration",
"data": { "varName": "CorrespondenceRecipient" },
"fix": { "range": [693, 780], "text": "" },
"desc": "Remove unused import declaration."
}
]
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 22,
"column": 27,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 22,
"endColumn": 30,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [1400, 1403], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [1400, 1403], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 23,
"column": 21,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 23,
"endColumn": 24,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [1426, 1429], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [1426, 1429], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'dataSource' is assigned a value but never used. Allowed unused vars must match /^_/u.",
"line": 24,
"column": 7,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 24,
"endColumn": 17
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 24,
"column": 19,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 24,
"endColumn": 22,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [1450, 1453], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [1450, 1453], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-unsafe-assignment",
"severity": 2,
"message": "Unsafe assignment of an `any` value.",
"line": 133,
"column": 5,
"nodeType": "AssignmentExpression",
"messageId": "anyAssignment",
"endLine": 133,
"endColumn": 72
},
{
"ruleId": "@typescript-eslint/no-unsafe-assignment",
"severity": 2,
"message": "Unsafe assignment of an `any` value.",
"line": 134,
"column": 5,
"nodeType": "AssignmentExpression",
"messageId": "anyAssignment",
"endLine": 134,
"endColumn": 74
},
{
"ruleId": "@typescript-eslint/no-unsafe-assignment",
"severity": 2,
"message": "Unsafe assignment of an `any` value.",
"line": 135,
"column": 5,
"nodeType": "AssignmentExpression",
"messageId": "anyAssignment",
"endLine": 135,
"endColumn": 40
},
{
"ruleId": "@typescript-eslint/no-unsafe-assignment",
"severity": 2,
"message": "Unsafe assignment of an `any` value.",
"line": 144,
"column": 13,
"nodeType": "VariableDeclarator",
"messageId": "anyAssignment",
"endLine": 144,
"endColumn": 72
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 144,
"column": 69,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 144,
"endColumn": 72,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [5152, 5155], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [5152, 5155], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'mockStatus' is assigned a value but never used. Allowed unused vars must match /^_/u.",
"line": 154,
"column": 13,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 154,
"endColumn": 23
},
{
"ruleId": "@typescript-eslint/no-unsafe-argument",
"severity": 2,
"message": "Unsafe argument of type `any` assigned to a parameter of type `UpdateCorrespondenceDto`.",
"line": 177,
"column": 31,
"nodeType": "TSAsExpression",
"messageId": "unsafeArgument",
"endLine": 177,
"endColumn": 47
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 177,
"column": 44,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 177,
"endColumn": 47,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [6388, 6391], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [6388, 6391], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-unsafe-argument",
"severity": 2,
"message": "Unsafe argument of type `any` assigned to a parameter of type `User`.",
"line": 177,
"column": 49,
"nodeType": "Identifier",
"messageId": "unsafeArgument",
"endLine": 177,
"endColumn": 57
},
{
"ruleId": "@typescript-eslint/unbound-method",
"severity": 2,
"message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.",
"line": 180,
"column": 14,
"nodeType": "MemberExpression",
"messageId": "unboundWithoutThisAnnotation",
"endLine": 180,
"endColumn": 51
},
{
"ruleId": "@typescript-eslint/no-unsafe-assignment",
"severity": 2,
"message": "Unsafe assignment of an `any` value.",
"line": 184,
"column": 13,
"nodeType": "VariableDeclarator",
"messageId": "anyAssignment",
"endLine": 184,
"endColumn": 72
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 184,
"column": 69,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 184,
"endColumn": 72,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [6692, 6695], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [6692, 6695], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-unsafe-argument",
"severity": 2,
"message": "Unsafe argument of type `any` assigned to a parameter of type `UpdateCorrespondenceDto`.",
"line": 208,
"column": 31,
"nodeType": "TSAsExpression",
"messageId": "unsafeArgument",
"endLine": 208,
"endColumn": 47
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 208,
"column": 44,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 208,
"endColumn": 47,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [7419, 7422], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [7419, 7422], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-unsafe-argument",
"severity": 2,
"message": "Unsafe argument of type `any` assigned to a parameter of type `User`.",
"line": 208,
"column": 49,
"nodeType": "Identifier",
"messageId": "unsafeArgument",
"endLine": 208,
"endColumn": 57
},
{
"ruleId": "@typescript-eslint/unbound-method",
"severity": 2,
"message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.",
"line": 210,
"column": 14,
"nodeType": "MemberExpression",
"messageId": "unboundWithoutThisAnnotation",
"endLine": 210,
"endColumn": 51
},
{
"ruleId": "@typescript-eslint/no-unsafe-assignment",
"severity": 2,
"message": "Unsafe assignment of an `any` value.",
"line": 213,
"column": 13,
"nodeType": "VariableDeclarator",
"messageId": "anyAssignment",
"endLine": 213,
"endColumn": 72
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 213,
"column": 69,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 213,
"endColumn": 72,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [7663, 7666], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [7663, 7666], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-unsafe-argument",
"severity": 2,
"message": "Unsafe argument of type `any` assigned to a parameter of type `UpdateCorrespondenceDto`.",
"line": 237,
"column": 31,
"nodeType": "TSAsExpression",
"messageId": "unsafeArgument",
"endLine": 237,
"endColumn": 47
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 237,
"column": 44,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 237,
"endColumn": 47,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [8372, 8375], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [8372, 8375], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-unsafe-argument",
"severity": 2,
"message": "Unsafe argument of type `any` assigned to a parameter of type `User`.",
"line": 237,
"column": 49,
"nodeType": "Identifier",
"messageId": "unsafeArgument",
"endLine": 237,
"endColumn": 57
},
{
"ruleId": "@typescript-eslint/unbound-method",
"severity": 2,
"message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.",
"line": 239,
"column": 14,
"nodeType": "MemberExpression",
"messageId": "unboundWithoutThisAnnotation",
"endLine": 239,
"endColumn": 51
},
{
"ruleId": "@typescript-eslint/no-unsafe-assignment",
"severity": 2,
"message": "Unsafe assignment of an `any` value.",
"line": 243,
"column": 13,
"nodeType": "VariableDeclarator",
"messageId": "anyAssignment",
"endLine": 243,
"endColumn": 72
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 243,
"column": 69,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 243,
"endColumn": 72,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [8627, 8630], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [8627, 8630], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-unsafe-call",
"severity": 2,
"message": "Unsafe call of a type that could not be resolved.",
"line": 262,
"column": 7,
"nodeType": "MemberExpression",
"messageId": "errorCall",
"endLine": 264,
"endColumn": 27
},
{
"ruleId": "@typescript-eslint/no-unsafe-member-access",
"severity": 2,
"message": "Unsafe member access .mockResolvedValue on a type that cannot be resolved.",
"line": 264,
"column": 10,
"nodeType": "Identifier",
"messageId": "errorMemberExpression",
"endLine": 264,
"endColumn": 27
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 264,
"column": 71,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 264,
"endColumn": 74,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [9355, 9358], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [9355, 9358], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-unsafe-argument",
"severity": 2,
"message": "Unsafe argument of type `any` assigned to a parameter of type `UpdateCorrespondenceDto`.",
"line": 270,
"column": 31,
"nodeType": "TSAsExpression",
"messageId": "unsafeArgument",
"endLine": 270,
"endColumn": 47
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 270,
"column": 44,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 270,
"endColumn": 47,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [9525, 9528], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [9525, 9528], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-unsafe-argument",
"severity": 2,
"message": "Unsafe argument of type `any` assigned to a parameter of type `User`.",
"line": 270,
"column": 49,
"nodeType": "Identifier",
"messageId": "unsafeArgument",
"endLine": 270,
"endColumn": 57
},
{
"ruleId": "@typescript-eslint/unbound-method",
"severity": 2,
"message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.",
"line": 272,
"column": 14,
"nodeType": "MemberExpression",
"messageId": "unboundWithoutThisAnnotation",
"endLine": 272,
"endColumn": 51
}
],
"suppressedMessages": [],
"errorCount": 36,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import { Test, TestingModule } from '@nestjs/testing';\r\nimport { getRepositoryToken } from '@nestjs/typeorm';\r\nimport { DataSource } from 'typeorm';\r\nimport { CorrespondenceService } from './correspondence.service';\r\nimport { Correspondence } from './entities/correspondence.entity';\r\nimport { CorrespondenceRevision } from './entities/correspondence-revision.entity';\r\nimport { CorrespondenceType } from './entities/correspondence-type.entity';\r\nimport { CorrespondenceStatus } from './entities/correspondence-status.entity';\r\nimport { CorrespondenceReference } from './entities/correspondence-reference.entity';\r\nimport { Organization } from '../organization/entities/organization.entity';\r\nimport { CorrespondenceRecipient } from './entities/correspondence-recipient.entity';\r\nimport { DocumentNumberingService } from '../document-numbering/services/document-numbering.service';\r\nimport { JsonSchemaService } from '../json-schema/json-schema.service';\r\nimport { WorkflowEngineService } from '../workflow-engine/workflow-engine.service';\r\nimport { UserService } from '../user/user.service';\r\nimport { SearchService } from '../search/search.service';\r\nimport { FileStorageService } from '../../common/file-storage/file-storage.service';\r\n\r\ndescribe('CorrespondenceService', () => {\r\n let service: CorrespondenceService;\r\n let numberingService: DocumentNumberingService;\r\n let correspondenceRepo: any;\r\n let revisionRepo: any;\r\n let dataSource: any;\r\n\r\n const createMockRepository = () => ({\r\n find: jest.fn(),\r\n findOne: jest.fn(),\r\n create: jest.fn(),\r\n save: jest.fn(),\r\n update: jest.fn(),\r\n delete: jest.fn(),\r\n softDelete: jest.fn(),\r\n createQueryBuilder: jest.fn(() => ({\r\n leftJoinAndSelect: jest.fn().mockReturnThis(),\r\n where: jest.fn().mockReturnThis(),\r\n andWhere: jest.fn().mockReturnThis(),\r\n orderBy: jest.fn().mockReturnThis(),\r\n skip: jest.fn().mockReturnThis(),\r\n take: jest.fn().mockReturnThis(),\r\n getOne: jest.fn().mockResolvedValue(null),\r\n getMany: jest.fn().mockResolvedValue([]),\r\n getManyAndCount: jest.fn().mockResolvedValue([[], 0]),\r\n })),\r\n });\r\n\r\n const mockDataSource = {\r\n createQueryRunner: jest.fn(() => ({\r\n connect: jest.fn(),\r\n startTransaction: jest.fn(),\r\n commitTransaction: jest.fn(),\r\n rollbackTransaction: jest.fn(),\r\n release: jest.fn(),\r\n manager: {\r\n create: jest.fn(),\r\n save: jest.fn(),\r\n findOne: jest.fn(),\r\n },\r\n })),\r\n getRepository: jest.fn(() => createMockRepository()),\r\n };\r\n\r\n beforeEach(async () => {\r\n const module: TestingModule = await Test.createTestingModule({\r\n providers: [\r\n CorrespondenceService,\r\n {\r\n provide: getRepositoryToken(Correspondence),\r\n useValue: createMockRepository(),\r\n },\r\n {\r\n provide: getRepositoryToken(CorrespondenceRevision),\r\n useValue: createMockRepository(),\r\n },\r\n {\r\n provide: getRepositoryToken(CorrespondenceType),\r\n useValue: createMockRepository(),\r\n },\r\n {\r\n provide: getRepositoryToken(CorrespondenceStatus),\r\n useValue: createMockRepository(),\r\n },\r\n {\r\n provide: getRepositoryToken(CorrespondenceReference),\r\n useValue: createMockRepository(),\r\n },\r\n {\r\n provide: getRepositoryToken(Organization),\r\n useValue: createMockRepository(),\r\n },\r\n {\r\n provide: DocumentNumberingService,\r\n useValue: {\r\n generateNextNumber: jest.fn(),\r\n updateNumberForDraft: jest.fn(),\r\n previewNextNumber: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: JsonSchemaService,\r\n useValue: { validate: jest.fn() },\r\n },\r\n {\r\n provide: WorkflowEngineService,\r\n useValue: { createInstance: jest.fn() },\r\n },\r\n {\r\n provide: UserService,\r\n useValue: {\r\n findOne: jest.fn(),\r\n getUserPermissions: jest.fn().mockResolvedValue([]),\r\n },\r\n },\r\n {\r\n provide: DataSource,\r\n useValue: mockDataSource,\r\n },\r\n {\r\n provide: SearchService,\r\n useValue: { indexDocument: jest.fn() },\r\n },\r\n {\r\n provide: FileStorageService,\r\n useValue: { commit: jest.fn().mockResolvedValue([]) },\r\n },\r\n ],\r\n }).compile();\r\n\r\n service = module.get<CorrespondenceService>(CorrespondenceService);\r\n numberingService = module.get<DocumentNumberingService>(\r\n DocumentNumberingService\r\n );\r\n correspondenceRepo = module.get(getRepositoryToken(Correspondence));\r\n revisionRepo = module.get(getRepositoryToken(CorrespondenceRevision));\r\n dataSource = module.get(DataSource);\r\n });\r\n\r\n it('should be defined', () => {\r\n expect(service).toBeDefined();\r\n });\r\n\r\n describe('update', () => {\r\n it('should NOT regenerate number if critical fields unchanged', async () => {\r\n const mockUser = { user_id: 1, primaryOrganizationId: 10 } as any;\r\n const mockRevision = {\r\n id: 100,\r\n correspondenceId: 1,\r\n isCurrent: true,\r\n statusId: 5,\r\n }; // Status 5 = Draft handled by logic?\r\n // Mock status repo to return DRAFT\r\n // But strict logic: revision.statusId check\r\n jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(mockRevision);\r\n const mockStatus = { id: 5, statusCode: 'DRAFT' };\r\n // Need to set statusRepo mock behavior... simplified here for brevity or assume defaults\r\n // Injecting internal access to statusRepo is hard without `module.get` if I didn't save it.\r\n // Let's assume it passes check for now.\r\n\r\n const mockCorr = {\r\n id: 1,\r\n projectId: 1,\r\n correspondenceTypeId: 2,\r\n disciplineId: 3,\r\n originatorId: 10,\r\n correspondenceNumber: 'OLD-NUM',\r\n recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }],\r\n };\r\n jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue(mockCorr);\r\n\r\n // Update DTO with same values\r\n const updateDto = {\r\n projectId: 1,\r\n disciplineId: 3,\r\n // recipients missing -> imply no change\r\n };\r\n\r\n await service.update(1, updateDto as any, mockUser);\r\n\r\n // Check that updateNumberForDraft was NOT called\r\n expect(numberingService.updateNumberForDraft).not.toHaveBeenCalled();\r\n });\r\n\r\n it('should regenerate number if Project ID changes', async () => {\r\n const mockUser = { user_id: 1, primaryOrganizationId: 10 } as any;\r\n const mockRevision = {\r\n id: 100,\r\n correspondenceId: 1,\r\n isCurrent: true,\r\n statusId: 5,\r\n };\r\n jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(mockRevision);\r\n\r\n const mockCorr = {\r\n id: 1,\r\n projectId: 1, // Old Project\r\n correspondenceTypeId: 2,\r\n disciplineId: 3,\r\n originatorId: 10,\r\n correspondenceNumber: 'OLD-NUM',\r\n recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }],\r\n };\r\n jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue(mockCorr);\r\n\r\n const updateDto = {\r\n projectId: 2, // New Project -> Change!\r\n };\r\n\r\n await service.update(1, updateDto as any, mockUser);\r\n\r\n expect(numberingService.updateNumberForDraft).toHaveBeenCalled();\r\n });\r\n it('should regenerate number if Document Type changes', async () => {\r\n const mockUser = { user_id: 1, primaryOrganizationId: 10 } as any;\r\n const mockRevision = {\r\n id: 100,\r\n correspondenceId: 1,\r\n isCurrent: true,\r\n statusId: 5,\r\n };\r\n jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(mockRevision);\r\n\r\n const mockCorr = {\r\n id: 1,\r\n projectId: 1,\r\n correspondenceTypeId: 2, // Old Type\r\n disciplineId: 3,\r\n originatorId: 10,\r\n correspondenceNumber: 'OLD-NUM',\r\n recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }],\r\n };\r\n jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue(mockCorr);\r\n\r\n const updateDto = {\r\n typeId: 999, // New Type\r\n };\r\n\r\n await service.update(1, updateDto as any, mockUser);\r\n\r\n expect(numberingService.updateNumberForDraft).toHaveBeenCalled();\r\n });\r\n\r\n it('should regenerate number if Recipient Organization changes', async () => {\r\n const mockUser = { user_id: 1, primaryOrganizationId: 10 } as any;\r\n const mockRevision = {\r\n id: 100,\r\n correspondenceId: 1,\r\n isCurrent: true,\r\n statusId: 5,\r\n };\r\n jest.spyOn(revisionRepo, 'findOne').mockResolvedValue(mockRevision);\r\n\r\n const mockCorr = {\r\n id: 1,\r\n projectId: 1,\r\n correspondenceTypeId: 2,\r\n disciplineId: 3,\r\n originatorId: 10,\r\n correspondenceNumber: 'OLD-NUM',\r\n recipients: [{ recipientType: 'TO', recipientOrganizationId: 99 }], // Old Recipient 99\r\n };\r\n jest.spyOn(correspondenceRepo, 'findOne').mockResolvedValue(mockCorr);\r\n jest\r\n .spyOn(service['orgRepo'], 'findOne')\r\n .mockResolvedValue({ id: 88, organizationCode: 'NEW-ORG' } as any);\r\n\r\n const updateDto = {\r\n recipients: [{ type: 'TO', organizationId: 88 }], // New Recipient 88\r\n };\r\n\r\n await service.update(1, updateDto as any, mockUser);\r\n\r\n expect(numberingService.updateNumberForDraft).toHaveBeenCalled();\r\n });\r\n });\r\n});\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\correspondence.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\dto\\add-reference.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\dto\\create-correspondence.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\dto\\create-routing-template.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\dto\\search-correspondence.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\dto\\submit-correspondence.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\dto\\update-correspondence.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\dto\\workflow-action.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\correspondence-recipient.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\correspondence-reference.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\correspondence-revision.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\correspondence-routing.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\correspondence-status.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\correspondence-sub-type.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\correspondence-type.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\correspondence.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\routing-template-step.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\correspondence\\entities\\routing-template.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\dashboard\\dashboard.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\dashboard\\dashboard.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\dashboard\\dashboard.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\dashboard\\dto\\dashboard-stats.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\dashboard\\dto\\get-activity.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\dashboard\\dto\\get-pending.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\dashboard\\dto\\index.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\controllers\\document-numbering-admin.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\controllers\\document-numbering.controller.ts",
"messages": [
{
"ruleId": null,
"nodeType": null,
"fatal": true,
"severity": 2,
"message": "Parsing error: Declaration or statement expected.",
"line": 122,
"column": 4
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 1,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import {\r\n Controller,\r\n Get,\r\n Post,\r\n Patch,\r\n Param,\r\n Body,\r\n UseGuards,\r\n Query,\r\n ParseIntPipe,\r\n} from '@nestjs/common';\r\nimport {\r\n ApiTags,\r\n ApiOperation,\r\n ApiResponse,\r\n ApiBearerAuth,\r\n ApiQuery,\r\n} from '@nestjs/swagger';\r\nimport { JwtAuthGuard } from '../../../common/guards/jwt-auth.guard';\r\nimport { RbacGuard } from '../../../common/guards/rbac.guard';\r\nimport { RequirePermission } from '../../../common/decorators/require-permission.decorator';\r\nimport { DocumentNumberingService } from '../services/document-numbering.service';\r\nimport { PreviewNumberDto } from '../dto/preview-number.dto';\r\n\r\n@ApiTags('Document Numbering')\r\n@ApiBearerAuth()\r\n@Controller('document-numbering')\r\n@UseGuards(JwtAuthGuard, RbacGuard)\r\nexport class DocumentNumberingController {\r\n constructor(private readonly numberingService: DocumentNumberingService) {}\r\n\r\n // ----------------------------------------------------------\r\n // Logs\r\n // ----------------------------------------------------------\r\n\r\n @Get('logs/audit')\r\n @ApiOperation({ summary: 'Get document generation audit logs' })\r\n @ApiResponse({ status: 200, description: 'List of audit logs' })\r\n @ApiQuery({ name: 'limit', required: false, type: Number })\r\n @RequirePermission('system.view_logs')\r\n getAuditLogs(@Query('limit') limit?: number) {\r\n return this.numberingService.getAuditLogs(limit ? Number(limit) : 100);\r\n }\r\n\r\n @Get('logs/errors')\r\n @ApiOperation({ summary: 'Get document generation error logs' })\r\n @ApiResponse({ status: 200, description: 'List of error logs' })\r\n @ApiQuery({ name: 'limit', required: false, type: Number })\r\n @RequirePermission('system.view_logs')\r\n getErrorLogs(@Query('limit') limit?: number) {\r\n return this.numberingService.getErrorLogs(limit ? Number(limit) : 100);\r\n }\r\n\r\n // ----------------------------------------------------------\r\n // Sequences / Counters\r\n // ----------------------------------------------------------\r\n\r\n @Get('sequences')\r\n @ApiOperation({ summary: 'Get all number sequences/counters' })\r\n @ApiResponse({ status: 200, description: 'List of counter sequences' })\r\n @ApiQuery({ name: 'projectId', required: false, type: Number })\r\n @RequirePermission('correspondence.read')\r\n getSequences(@Query('projectId') projectId?: number) {\r\n return this.numberingService.getSequences(\r\n projectId ? Number(projectId) : undefined\r\n );\r\n }\r\n\r\n @Patch('counters/:id')\r\n @ApiOperation({ summary: 'Update counter sequence value (Admin only)' })\r\n @RequirePermission('numbering.manage_formats')\r\n async updateCounter(\r\n @Param('id', ParseIntPipe) id: number,\r\n @Body('sequence') sequence: number\r\n ) {\r\n return this.numberingService.setCounterValue(id, sequence);\r\n }\r\n\r\n // ----------------------------------------------------------\r\n // Preview / Test\r\n // ----------------------------------------------------------\r\n\r\n @Post('preview')\r\n @ApiOperation({ summary: 'Preview what a document number would look like' })\r\n @ApiResponse({\r\n status: 200,\r\n description: 'Preview result without incrementing counter',\r\n })\r\n @RequirePermission('correspondence.read')\r\n async previewNumber(@Body() dto: PreviewNumberDto) {\r\n // ADR-019: Resolve UUID→INT for project and organization IDs\r\n const resolvedProjectId = await this.numberingService.resolveIdForPreview(\r\n 'project',\r\n dto.projectId\r\n );\r\n const resolvedOriginatorId =\r\n await this.numberingService.resolveIdForPreview(\r\n 'organization',\r\n dto.originatorOrganizationId\r\n );\r\n const resolvedRecipientId = dto.recipientOrganizationId\r\n ? await this.numberingService.resolveIdForPreview(\r\n 'organization',\r\n dto.recipientOrganizationId\r\n )\r\n : undefined;\r\n\r\n const result = await this.numberingService.previewNumber({\r\n projectId: resolvedProjectId,\r\n originatorOrganizationId: resolvedOriginatorId,\r\n typeId: dto.correspondenceTypeId,\r\n subTypeId: dto.subTypeId,\r\n rfaTypeId: dto.rfaTypeId,\r\n disciplineId: dto.disciplineId,\r\n recipientOrganizationId: resolvedRecipientId,\r\n year: dto.year,\r\n customTokens: dto.customTokens,\r\n });\r\n// console.log(\r\n '[DocumentNumberingController] Preview result:',\r\n JSON.stringify(result)\r\n );\r\n return result;\r\n }\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\controllers\\numbering-metrics.controller.ts",
"messages": [
{
"ruleId": "@typescript-eslint/require-await",
"severity": 2,
"message": "Async method 'getMetrics' has no 'await' expression.",
"line": 13,
"column": 3,
"nodeType": "FunctionExpression",
"messageId": "missingAwait",
"endLine": 13,
"endColumn": 19,
"suggestions": [
{ "messageId": "removeAsync", "fix": { "range": [513, 519], "text": "" }, "desc": "Remove 'async'." }
]
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import { Controller, Get, _UseGuards } from '@nestjs/common';\nimport { MetricsService } from '../services/metrics.service';\n// import { PermissionGuard } from '../../auth/guards/permission.guard';\n// import { Permissions } from '../../auth/decorators/permissions.decorator';\n\n@Controller('admin/document-numbering/metrics')\n// @UseGuards(PermissionGuard)\nexport class NumberingMetricsController {\n constructor(private readonly metricsService: MetricsService) {}\n\n @Get()\n // @Permissions('system.view_logs')\n async getMetrics() {\n // Determine how to return metrics.\n // Standard Prometheus metrics are usually exposed via a separate /metrics endpoint processing all metrics.\n // If the frontend needs JSON data, we might need to query the current values from the registry or metrics service.\n\n // For now, returning a simple status or aggregated view if supported by MetricsService,\n // otherwise this might be a placeholder for a custom dashboard API.\n return {\n status: 'Metrics are being collected',\n // TODO: Implement custom JSON export of metric values if needed for custom dashboard\n };\n }\n}\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\document-numbering.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\document-numbering.service.spec.ts",
"messages": [
{
"ruleId": "@typescript-eslint/unbound-method",
"severity": 2,
"message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.",
"line": 127,
"column": 14,
"nodeType": "MemberExpression",
"messageId": "unboundWithoutThisAnnotation",
"endLine": 127,
"endColumn": 45
},
{
"ruleId": "@typescript-eslint/unbound-method",
"severity": 2,
"message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.",
"line": 128,
"column": 14,
"nodeType": "MemberExpression",
"messageId": "unboundWithoutThisAnnotation",
"endLine": 128,
"endColumn": 34
},
{
"ruleId": "@typescript-eslint/no-unsafe-assignment",
"severity": 2,
"message": "Unsafe assignment of an `any` value.",
"line": 145,
"column": 13,
"nodeType": "VariableDeclarator",
"messageId": "anyAssignment",
"endLine": 145,
"endColumn": 76
},
{
"ruleId": "@typescript-eslint/no-unsafe-member-access",
"severity": 2,
"message": "Unsafe member access .findOne on an `any` value.",
"line": 146,
"column": 18,
"nodeType": "Identifier",
"messageId": "unsafeMemberExpression",
"endLine": 146,
"endColumn": 25
},
{
"ruleId": "@typescript-eslint/no-unsafe-member-access",
"severity": 2,
"message": "Unsafe member access .save on an `any` value.",
"line": 151,
"column": 18,
"nodeType": "Identifier",
"messageId": "unsafeMemberExpression",
"endLine": 151,
"endColumn": 22
},
{
"ruleId": "@typescript-eslint/no-unsafe-member-access",
"severity": 2,
"message": "Unsafe member access .save on an `any` value.",
"line": 159,
"column": 24,
"nodeType": "Identifier",
"messageId": "unsafeMemberExpression",
"endLine": 159,
"endColumn": 28
},
{
"ruleId": "@typescript-eslint/no-unsafe-assignment",
"severity": 2,
"message": "Unsafe assignment of an `any` value.",
"line": 163,
"column": 13,
"nodeType": "VariableDeclarator",
"messageId": "anyAssignment",
"endLine": 163,
"endColumn": 76
},
{
"ruleId": "@typescript-eslint/no-unsafe-member-access",
"severity": 2,
"message": "Unsafe member access .findOne on an `any` value.",
"line": 164,
"column": 18,
"nodeType": "Identifier",
"messageId": "unsafeMemberExpression",
"endLine": 164,
"endColumn": 25
},
{
"ruleId": "@typescript-eslint/no-unsafe-member-access",
"severity": 2,
"message": "Unsafe member access .save on an `any` value.",
"line": 168,
"column": 18,
"nodeType": "Identifier",
"messageId": "unsafeMemberExpression",
"endLine": 168,
"endColumn": 22
},
{
"ruleId": "@typescript-eslint/no-unsafe-member-access",
"severity": 2,
"message": "Unsafe member access .save on an `any` value.",
"line": 176,
"column": 24,
"nodeType": "Identifier",
"messageId": "unsafeMemberExpression",
"endLine": 176,
"endColumn": 28
}
],
"suppressedMessages": [],
"errorCount": 10,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import { Test, TestingModule } from '@nestjs/testing';\r\nimport { DocumentNumberingService } from './services/document-numbering.service';\r\nimport { CounterService } from './services/counter.service';\r\nimport { ReservationService } from './services/reservation.service';\r\nimport { FormatService } from './services/format.service';\r\nimport { getRepositoryToken } from '@nestjs/typeorm';\r\nimport { ConfigService } from '@nestjs/config';\r\nimport { DocumentNumberFormat } from './entities/document-number-format.entity';\r\nimport { DocumentNumberAudit } from './entities/document-number-audit.entity';\r\nimport { DocumentNumberError } from './entities/document-number-error.entity';\r\n\r\nimport { DocumentNumberingLockService } from './services/document-numbering-lock.service';\r\nimport { ManualOverrideService } from './services/manual-override.service';\r\nimport { MetricsService } from './services/metrics.service';\r\n\r\ndescribe('DocumentNumberingService', () => {\r\n let service: DocumentNumberingService;\r\n let module: TestingModule;\r\n let counterService: CounterService;\r\n let formatService: FormatService;\r\n\r\n const mockContext = {\r\n projectId: 1,\r\n originatorOrganizationId: 1,\r\n recipientOrganizationId: 1,\r\n typeId: 1,\r\n subTypeId: 1,\r\n rfaTypeId: 1,\r\n disciplineId: 1,\r\n year: 2025,\r\n customTokens: { TYPE_CODE: 'COR', ORG_CODE: 'GGL' },\r\n };\r\n\r\n beforeEach(async () => {\r\n module = await Test.createTestingModule({\r\n providers: [\r\n DocumentNumberingService,\r\n {\r\n provide: ConfigService,\r\n useValue: { get: jest.fn().mockReturnValue('localhost') },\r\n },\r\n {\r\n provide: CounterService,\r\n useValue: {\r\n incrementCounter: jest.fn().mockResolvedValue(1),\r\n getCurrentSequence: jest.fn().mockResolvedValue(0),\r\n },\r\n },\r\n {\r\n provide: ReservationService,\r\n useValue: {\r\n reserve: jest.fn(),\r\n confirm: jest.fn(),\r\n cancel: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: FormatService,\r\n useValue: {\r\n format: jest.fn().mockResolvedValue('0001'),\r\n },\r\n },\r\n {\r\n provide: DocumentNumberingLockService,\r\n useValue: {\r\n acquireLock: jest.fn().mockResolvedValue({ release: jest.fn() }),\r\n releaseLock: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: ManualOverrideService,\r\n useValue: { applyOverride: jest.fn() },\r\n },\r\n {\r\n provide: MetricsService,\r\n useValue: {\r\n numbersGenerated: { inc: jest.fn() },\r\n lockFailures: { inc: jest.fn() },\r\n },\r\n },\r\n {\r\n provide: getRepositoryToken(DocumentNumberFormat),\r\n useValue: { findOne: jest.fn() },\r\n },\r\n {\r\n provide: getRepositoryToken(DocumentNumberAudit),\r\n useValue: {\r\n create: jest.fn().mockReturnValue({ id: 1 }),\r\n save: jest.fn().mockResolvedValue({ id: 1 }),\r\n findOne: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: getRepositoryToken(DocumentNumberError),\r\n useValue: {\r\n create: jest.fn().mockReturnValue({}),\r\n save: jest.fn().mockResolvedValue({}),\r\n },\r\n },\r\n ],\r\n }).compile();\r\n\r\n service = module.get<DocumentNumberingService>(DocumentNumberingService);\r\n counterService = module.get<CounterService>(CounterService);\r\n formatService = module.get<FormatService>(FormatService);\r\n });\r\n\r\n afterEach(() => {\r\n jest.clearAllMocks();\r\n });\r\n\r\n it('should be defined', () => {\r\n expect(service).toBeDefined();\r\n });\r\n\r\n describe('generateNextNumber', () => {\r\n it('should generate a new number successfully', async () => {\r\n (counterService.incrementCounter as jest.Mock).mockResolvedValue(1);\r\n (formatService.format as jest.Mock).mockResolvedValue('DOC-0001');\r\n\r\n const result = await service.generateNextNumber(mockContext);\r\n\r\n // Service returns object with number and auditId\r\n expect(result).toHaveProperty('number');\r\n expect(result).toHaveProperty('auditId');\r\n expect(result.number).toBe('DOC-0001');\r\n expect(counterService.incrementCounter).toHaveBeenCalled();\r\n expect(formatService.format).toHaveBeenCalled();\r\n });\r\n\r\n it('should throw error when increment fails', async () => {\r\n // Mock CounterService to throw error\r\n (counterService.incrementCounter as jest.Mock).mockRejectedValue(\r\n new Error('Transaction failed')\r\n );\r\n\r\n await expect(service.generateNextNumber(mockContext)).rejects.toThrow(\r\n 'Transaction failed'\r\n );\r\n });\r\n });\r\n\r\n describe('Admin Operations', () => {\r\n it('voidAndReplace should verify audit log exists', async () => {\r\n const auditRepo = module.get(getRepositoryToken(DocumentNumberAudit));\r\n (auditRepo.findOne as jest.Mock).mockResolvedValue({\r\n documentNumber: 'DOC-001',\r\n counterKey: JSON.stringify({ projectId: 1, correspondenceTypeId: 1 }),\r\n templateUsed: 'test',\r\n });\r\n (auditRepo.save as jest.Mock).mockResolvedValue({ id: 2 });\r\n\r\n const result = await service.voidAndReplace({\r\n documentNumber: 'DOC-001',\r\n reason: 'test',\r\n replace: false,\r\n });\r\n expect(result.status).toBe('VOIDED');\r\n expect(auditRepo.save).toHaveBeenCalled();\r\n });\r\n\r\n it('cancelNumber should log cancellation', async () => {\r\n const auditRepo = module.get(getRepositoryToken(DocumentNumberAudit));\r\n (auditRepo.findOne as jest.Mock).mockResolvedValue({\r\n documentNumber: 'DOC-002',\r\n counterKey: {},\r\n });\r\n (auditRepo.save as jest.Mock).mockResolvedValue({ id: 3 });\r\n\r\n const result = await service.cancelNumber({\r\n documentNumber: 'DOC-002',\r\n reason: 'bad',\r\n projectId: 1,\r\n });\r\n expect(result.status).toBe('CANCELLED');\r\n expect(auditRepo.save).toHaveBeenCalled();\r\n });\r\n });\r\n});\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\dto\\confirm-reservation.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\dto\\counter-key.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\dto\\manual-override.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\dto\\preview-number.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\dto\\reserve-number.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\entities\\document-number-audit.entity.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-redundant-type-constituents",
"severity": 2,
"message": "'unknown' overrides all other types in this union type.",
"line": 28,
"column": 42,
"nodeType": "TSUnknownKeyword",
"messageId": "overrides",
"endLine": 28,
"endColumn": 49
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import {\r\n Entity,\r\n PrimaryGeneratedColumn,\r\n Column,\r\n CreateDateColumn,\r\n Index,\r\n} from 'typeorm';\r\n\r\n@Entity('document_number_audit')\r\n@Index(['createdAt'])\r\n@Index(['userId'])\r\n@Index(['documentId'])\r\n@Index(['status'])\r\n@Index(['operation'])\r\n@Index(['documentNumber'])\r\n@Index(['reservationToken'])\r\nexport class DocumentNumberAudit {\r\n @PrimaryGeneratedColumn()\r\n id!: number;\r\n\r\n @Column({ name: 'document_id', nullable: true })\r\n documentId?: number;\r\n\r\n @Column({ name: 'document_number', length: 100 })\r\n documentNumber!: string;\r\n\r\n @Column({ name: 'counter_key', type: 'json' })\r\n counterKey!: Record<string, unknown> | unknown;\r\n\r\n @Column({ name: 'template_used', length: 200 })\r\n templateUsed!: string;\r\n\r\n @Column({\r\n name: 'operation',\r\n type: 'enum',\r\n enum: [\r\n 'RESERVE',\r\n 'CONFIRM',\r\n 'CANCEL',\r\n 'MANUAL_OVERRIDE',\r\n 'VOID',\r\n 'VOID_REPLACE',\r\n 'GENERATE',\r\n ],\r\n default: 'CONFIRM',\r\n })\r\n operation!: string;\r\n\r\n @Column({\r\n name: 'status',\r\n type: 'enum',\r\n enum: ['RESERVED', 'CONFIRMED', 'CANCELLED', 'VOID', 'MANUAL'],\r\n nullable: true,\r\n })\r\n status?: string;\r\n\r\n @Column({ name: 'reservation_token', length: 36, nullable: true })\r\n reservationToken?: string;\r\n\r\n @Column({ name: 'idempotency_key', length: 36, nullable: true })\r\n idempotencyKey?: string;\r\n\r\n @Column({ name: 'originator_organization_id', nullable: true })\r\n originatorOrganizationId?: number;\r\n\r\n @Column({ name: 'recipient_organization_id', nullable: true })\r\n recipientOrganizationId?: number;\r\n\r\n @Column({ name: 'old_value', type: 'text', nullable: true })\r\n oldValue?: string;\r\n\r\n @Column({ name: 'new_value', type: 'text', nullable: true })\r\n newValue?: string;\r\n\r\n @Column({ name: 'metadata', type: 'json', nullable: true })\r\n metadata?: Record<string, unknown>;\r\n\r\n @Column({ name: 'user_id', nullable: true })\r\n userId?: number;\r\n\r\n @Column({ name: 'ip_address', length: 45, nullable: true })\r\n ipAddress?: string;\r\n\r\n @Column({ name: 'user_agent', type: 'text', nullable: true })\r\n userAgent?: string;\r\n\r\n @Column({ name: 'is_success', default: true })\r\n isSuccess!: boolean;\r\n\r\n @Column({ name: 'retry_count', default: 0 })\r\n retryCount!: number;\r\n\r\n @Column({ name: 'lock_wait_ms', nullable: true })\r\n lockWaitMs?: number;\r\n\r\n @Column({ name: 'total_duration_ms', nullable: true })\r\n totalDurationMs?: number;\r\n\r\n @Column({\r\n name: 'fallback_used',\r\n type: 'enum',\r\n enum: ['NONE', 'DB_LOCK', 'RETRY'],\r\n default: 'NONE',\r\n })\r\n fallbackUsed?: string;\r\n\r\n @CreateDateColumn({ name: 'created_at' })\r\n createdAt!: Date;\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\entities\\document-number-counter.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\entities\\document-number-error.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\entities\\document-number-format.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\entities\\document-number-reservation.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\interfaces\\document-numbering.interface.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\audit.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\counter.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\document-numbering-lock.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\document-numbering.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\format.service.ts",
"messages": [
{
"ruleId": null,
"nodeType": null,
"fatal": true,
"severity": 2,
"message": "Parsing error: Declaration or statement expected.",
"line": 56,
"column": 4
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 1,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import { Injectable } from '@nestjs/common';\r\nimport { InjectRepository } from '@nestjs/typeorm';\r\nimport { Repository, IsNull } from 'typeorm';\r\nimport { DocumentNumberFormat } from '../entities/document-number-format.entity';\r\nimport { Project } from '../../project/entities/project.entity';\r\nimport { CorrespondenceType } from '../../correspondence/entities/correspondence-type.entity';\r\nimport { Organization } from '../../organization/entities/organization.entity';\r\nimport { Discipline } from '../../master/entities/discipline.entity';\r\n\r\nexport interface FormatOptions {\r\n projectId: number;\r\n correspondenceTypeId: number;\r\n subTypeId?: number;\r\n rfaTypeId?: number;\r\n disciplineId?: number;\r\n sequence: number;\r\n resetScope: string;\r\n year?: number;\r\n originatorOrganizationId: number;\r\n recipientOrganizationId?: number;\r\n}\r\n\r\nexport interface DecodedTokens {\r\n [key: string]: string;\r\n}\r\n\r\n@Injectable()\r\nexport class FormatService {\r\n constructor(\r\n @InjectRepository(DocumentNumberFormat)\r\n private formatRepo: Repository<DocumentNumberFormat>,\r\n @InjectRepository(Project)\r\n private projectRepo: Repository<Project>,\r\n @InjectRepository(CorrespondenceType)\r\n private typeRepo: Repository<CorrespondenceType>,\r\n @InjectRepository(Organization)\r\n private orgRepo: Repository<Organization>,\r\n @InjectRepository(Discipline)\r\n private disciplineRepo: Repository<Discipline>\r\n ) {}\r\n\r\n async format(\r\n options: FormatOptions\r\n ): Promise<{ previewNumber: string; isDefault: boolean }> {\r\n const { template, isDefault } = await this.resolveFormatAndScope(options);\r\n const currentYear = options.year || new Date().getFullYear();\r\n const tokens = await this.resolveTokens(options, currentYear);\r\n\r\n const previewNumber = this.replaceTokens(\r\n template,\r\n tokens,\r\n options.sequence\r\n );\r\n// console.log(\r\n `[FormatService] Generated: \"${previewNumber}\" | Template: \"${template}\" | isDefault: ${isDefault}`\r\n );\r\n return { previewNumber, isDefault };\r\n }\r\n\r\n // --- Helpers ---\r\n\r\n private async resolveFormatAndScope(options: FormatOptions): Promise<{\r\n template: string;\r\n resetSequenceYearly: boolean;\r\n isDefault: boolean;\r\n }> {\r\n // 1. Specific Format\r\n const specificFormat = await this.formatRepo.findOne({\r\n where: {\r\n projectId: options.projectId,\r\n correspondenceTypeId: options.correspondenceTypeId,\r\n },\r\n });\r\n if (specificFormat)\r\n return {\r\n template: specificFormat.formatTemplate,\r\n resetSequenceYearly: specificFormat.resetSequenceYearly,\r\n isDefault: false,\r\n };\r\n\r\n // 2. Default Format\r\n const defaultFormat = await this.formatRepo.findOne({\r\n where: { projectId: options.projectId, correspondenceTypeId: IsNull() },\r\n });\r\n if (defaultFormat)\r\n return {\r\n template: defaultFormat.formatTemplate,\r\n resetSequenceYearly: defaultFormat.resetSequenceYearly,\r\n isDefault: true,\r\n };\r\n\r\n // 3. Fallback\r\n return {\r\n template: '{ORG}-{RECIPIENT}-{SEQ:4}-{YEAR:BE}',\r\n resetSequenceYearly: true,\r\n isDefault: true,\r\n };\r\n }\r\n\r\n private async resolveTokens(\r\n options: FormatOptions,\r\n year: number\r\n ): Promise<DecodedTokens> {\r\n const [project, type, recipientCode, disciplineCode, orgCode] =\r\n await Promise.all([\r\n this.projectRepo.findOne({\r\n where: { id: options.projectId },\r\n select: ['projectCode'],\r\n }),\r\n this.typeRepo.findOne({\r\n where: { id: options.correspondenceTypeId },\r\n select: ['typeCode'],\r\n }),\r\n this.resolveRecipientCode(options.recipientOrganizationId),\r\n this.resolveDisciplineCode(options.disciplineId),\r\n this.resolveOrgCode(options.originatorOrganizationId),\r\n ]);\r\n\r\n return {\r\n '{PROJECT}': project?.projectCode || 'PROJ',\r\n '{TYPE}': type?.typeCode || 'DOC',\r\n '{ORG}': orgCode,\r\n '{RECIPIENT}': recipientCode,\r\n '{DISCIPLINE}': disciplineCode,\r\n '{YEAR}': year.toString(),\r\n '{YEAR:BE}': (year + 543).toString(),\r\n '{REV}': '0',\r\n };\r\n }\r\n\r\n private replaceTokens(\r\n template: string,\r\n tokens: DecodedTokens,\r\n sequence: number\r\n ): string {\r\n let result = template;\r\n for (const [key, value] of Object.entries(tokens)) {\r\n result = result.replace(\r\n new RegExp(key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'g'),\r\n value\r\n );\r\n }\r\n const seqMatch = result.match(/{SEQ:(\\d+)}/);\r\n if (seqMatch) {\r\n const padding = Number(seqMatch[1]);\r\n result = result.replace(\r\n seqMatch[0],\r\n sequence.toString().padStart(padding, '0')\r\n );\r\n }\r\n return result;\r\n }\r\n\r\n private async resolveRecipientCode(recipientId?: number): Promise<string> {\r\n if (!recipientId) return 'GEN';\r\n const org = await this.orgRepo.findOne({\r\n where: { id: recipientId },\r\n select: ['organizationCode'],\r\n });\r\n return org ? org.organizationCode : 'GEN';\r\n }\r\n\r\n private async resolveOrgCode(orgId?: number): Promise<string> {\r\n if (!orgId) return 'GEN';\r\n const org = await this.orgRepo.findOne({\r\n where: { id: orgId },\r\n select: ['organizationCode'],\r\n });\r\n return org ? org.organizationCode : 'GEN';\r\n }\r\n\r\n private async resolveDisciplineCode(disciplineId?: number): Promise<string> {\r\n if (!disciplineId) return 'GEN';\r\n const discipline = await this.disciplineRepo.findOne({\r\n where: { id: disciplineId },\r\n select: ['disciplineCode'],\r\n });\r\n return discipline ? discipline.disciplineCode : 'GEN';\r\n }\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\manual-override.service.spec.ts",
"messages": [
{
"ruleId": "@typescript-eslint/unbound-method",
"severity": 2,
"message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.",
"line": 56,
"column": 12,
"nodeType": "MemberExpression",
"messageId": "unboundWithoutThisAnnotation",
"endLine": 56,
"endColumn": 45
},
{
"ruleId": "@typescript-eslint/unbound-method",
"severity": 2,
"message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.",
"line": 57,
"column": 12,
"nodeType": "MemberExpression",
"messageId": "unboundWithoutThisAnnotation",
"endLine": 57,
"endColumn": 28
}
],
"suppressedMessages": [],
"errorCount": 2,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import { Test, TestingModule } from '@nestjs/testing';\r\nimport { ManualOverrideService } from './manual-override.service';\r\nimport { CounterService } from './counter.service';\r\nimport { AuditService } from './audit.service';\r\nimport { ManualOverrideDto } from '../dto/manual-override.dto';\r\n\r\ndescribe('ManualOverrideService', () => {\r\n let service: ManualOverrideService;\r\n let counterService: CounterService;\r\n let auditService: AuditService;\r\n\r\n const mockCounterService = {\r\n forceUpdateCounter: jest.fn(),\r\n };\r\n\r\n const mockAuditService = {\r\n log: jest.fn(),\r\n };\r\n\r\n beforeEach(async () => {\r\n const module: TestingModule = await Test.createTestingModule({\r\n providers: [\r\n ManualOverrideService,\r\n { provide: CounterService, useValue: mockCounterService },\r\n { provide: AuditService, useValue: mockAuditService },\r\n ],\r\n }).compile();\r\n\r\n service = module.get<ManualOverrideService>(ManualOverrideService);\r\n counterService = module.get<CounterService>(CounterService);\r\n auditService = module.get<AuditService>(AuditService);\r\n });\r\n\r\n afterEach(() => {\r\n jest.clearAllMocks();\r\n });\r\n\r\n it('should apply override and log audit', async () => {\r\n const dto: ManualOverrideDto = {\r\n projectId: 1,\r\n originatorOrganizationId: 2,\r\n recipientOrganizationId: 3,\r\n correspondenceTypeId: 4,\r\n subTypeId: 5,\r\n rfaTypeId: 6,\r\n disciplineId: 7,\r\n resetScope: 'YEAR_2024',\r\n newLastNumber: 999,\r\n reason: 'System sync',\r\n reference: 'TICKET-123',\r\n };\r\n const userId = 101;\r\n\r\n await service.applyOverride(dto, userId);\r\n\r\n expect(counterService.forceUpdateCounter).toHaveBeenCalledWith(dto, 999);\r\n expect(auditService.log).toHaveBeenCalledWith(\r\n expect.objectContaining({\r\n documentNumber: 'OVERRIDE-TO-999',\r\n operation: 'MANUAL_OVERRIDE',\r\n status: 'MANUAL',\r\n userId: userId,\r\n metadata: { reason: 'System sync', reference: 'TICKET-123' },\r\n })\r\n );\r\n });\r\n});\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\manual-override.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\metrics.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\reservation.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\document-numbering\\services\\template.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\asbuilt-drawing.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\asbuilt-drawing.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\contract-drawing.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\contract-drawing.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\drawing-master-data.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\drawing-master-data.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\drawing.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\dto\\create-asbuilt-drawing-revision.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\dto\\create-asbuilt-drawing.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\dto\\create-contract-drawing.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\dto\\create-shop-drawing-revision.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\dto\\create-shop-drawing.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\dto\\search-asbuilt-drawing.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\dto\\search-contract-drawing.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\dto\\search-shop-drawing.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\dto\\update-contract-drawing.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\asbuilt-drawing-revision.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\asbuilt-drawing.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\contract-drawing-category.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\contract-drawing-sub-category.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\contract-drawing-subcat-cat-map.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\contract-drawing-volume.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\contract-drawing.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\shop-drawing-main-category.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\shop-drawing-revision.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\shop-drawing-sub-category.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\entities\\shop-drawing.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\shop-drawing.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\drawing\\shop-drawing.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\dto\\create-json-schema.dto.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 51,
"column": 37,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 51,
"endColumn": 40,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [966, 969], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [966, 969], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 55,
"column": 29,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 55,
"endColumn": 32,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [1035, 1038], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [1035, 1038], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 65,
"column": 36,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 65,
"endColumn": 39,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [1264, 1267], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [1264, 1267], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
}
],
"suppressedMessages": [],
"errorCount": 3,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "// File: src/modules/json-schema/dto/create-json-schema.dto.ts\r\nimport {\r\n IsString,\r\n IsNotEmpty,\r\n IsInt,\r\n IsOptional,\r\n IsBoolean,\r\n IsObject,\r\n IsArray,\r\n ValidateNested,\r\n} from 'class-validator';\r\nimport { Type } from 'class-transformer';\r\n\r\nexport class VirtualColumnConfigDto {\r\n @IsString()\r\n @IsNotEmpty()\r\n json_path!: string;\r\n\r\n @IsString()\r\n @IsNotEmpty()\r\n column_name!: string;\r\n\r\n @IsString()\r\n @IsNotEmpty()\r\n data_type!: 'INT' | 'VARCHAR' | 'BOOLEAN' | 'DATE' | 'DECIMAL' | 'DATETIME';\r\n\r\n @IsString()\r\n @IsOptional()\r\n index_type?: 'INDEX' | 'UNIQUE' | 'FULLTEXT';\r\n\r\n @IsBoolean()\r\n @IsOptional()\r\n is_required?: boolean;\r\n}\r\n\r\nexport class CreateJsonSchemaDto {\r\n @IsString()\r\n @IsNotEmpty()\r\n schemaCode!: string;\r\n\r\n @IsString() // ✅ เพิ่ม Validation\r\n @IsNotEmpty()\r\n tableName!: string;\r\n\r\n @IsInt()\r\n @IsOptional()\r\n version?: number;\r\n\r\n @IsObject()\r\n @IsNotEmpty()\r\n schemaDefinition!: Record<string, any>;\r\n\r\n @IsObject()\r\n @IsOptional()\r\n uiSchema?: Record<string, any>;\r\n\r\n @IsArray()\r\n @IsOptional()\r\n @ValidateNested({ each: true })\r\n @Type(() => VirtualColumnConfigDto)\r\n virtualColumns?: VirtualColumnConfigDto[];\r\n\r\n @IsObject()\r\n @IsOptional()\r\n migrationScript?: Record<string, any>;\r\n\r\n @IsBoolean()\r\n @IsOptional()\r\n isActive?: boolean;\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\dto\\migrate-data.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\dto\\search-json-schema.dto.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-unsafe-return",
"severity": 2,
"message": "Unsafe return of a value of type `any`.",
"line": 14,
"column": 5,
"nodeType": "ReturnStatement",
"messageId": "unsafeReturn",
"endLine": 14,
"endColumn": 18
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import { IsString, IsOptional, IsBoolean, IsInt } from 'class-validator';\r\nimport { Type, Transform } from 'class-transformer';\r\n\r\nexport class SearchJsonSchemaDto {\r\n @IsString()\r\n @IsOptional()\r\n search?: string; // ค้นหาจาก schemaCode\r\n\r\n @IsBoolean()\r\n @IsOptional()\r\n @Transform(({ value }) => {\r\n if (value === 'true') return true;\r\n if (value === 'false') return false;\r\n return value;\r\n })\r\n isActive?: boolean;\r\n\r\n @IsOptional()\r\n @IsInt()\r\n @Type(() => Number)\r\n page: number = 1;\r\n\r\n @IsOptional()\r\n @IsInt()\r\n @Type(() => Number)\r\n limit: number = 20;\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\dto\\update-json-schema.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\entities\\json-schema.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\interfaces\\ui-schema.interface.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\interfaces\\validation-result.interface.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\json-schema.controller.spec.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\json-schema.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\json-schema.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\json-schema.service.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-unsafe-assignment",
"severity": 2,
"message": "Unsafe assignment of an `any` value.",
"line": 265,
"column": 11,
"nodeType": "VariableDeclarator",
"messageId": "anyAssignment",
"endLine": 267,
"endColumn": 6
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "// File: src/modules/json-schema/json-schema.service.ts\r\n// บันทึกการแก้ไข: Fix TS2345 (undefined check)\r\n\r\nimport {\r\n BadRequestException,\r\n Injectable,\r\n Logger,\r\n NotFoundException,\r\n OnModuleInit,\r\n} from '@nestjs/common';\r\nimport { InjectRepository } from '@nestjs/typeorm';\r\nimport Ajv, { ValidateFunction } from 'ajv';\r\nimport addFormats from 'ajv-formats';\r\nimport { Repository } from 'typeorm';\r\n\r\nimport { CreateJsonSchemaDto } from './dto/create-json-schema.dto';\r\nimport { SearchJsonSchemaDto } from './dto/search-json-schema.dto';\r\nimport { UpdateJsonSchemaDto } from './dto/update-json-schema.dto';\r\nimport { JsonSchema } from './entities/json-schema.entity';\r\n\r\n// Services ย่อยที่แยกตามหน้าที่ (Single Responsibility)\r\nimport {\r\n JsonSecurityService,\r\n SecurityContext,\r\n} from './services/json-security.service';\r\nimport { UiSchemaService } from './services/ui-schema.service';\r\nimport { UiSchema } from './interfaces/ui-schema.interface';\r\nimport { VirtualColumnService } from './services/virtual-column.service';\r\n\r\nimport {\r\n ValidationErrorDetail,\r\n ValidationOptions,\r\n ValidationResult,\r\n} from './interfaces/validation-result.interface';\r\n\r\n@Injectable()\r\nexport class JsonSchemaService implements OnModuleInit {\r\n private ajv: Ajv;\r\n private validators = new Map<string, ValidateFunction>(); // Cache สำหรับเก็บ Validator ที่ Compile แล้ว\r\n private readonly logger = new Logger(JsonSchemaService.name);\r\n\r\n // ค่า Default สำหรับการตรวจสอบข้อมูล\r\n private readonly defaultOptions: ValidationOptions = {\r\n removeAdditional: true, // ลบฟิลด์เกิน\r\n coerceTypes: true, // แปลงชนิดข้อมูลอัตโนมัติ (เช่น \"123\" -> 123)\r\n useDefaults: true, // ใส่ค่า Default ถ้าไม่มีข้อมูล\r\n };\r\n\r\n constructor(\r\n @InjectRepository(JsonSchema)\r\n private readonly jsonSchemaRepository: Repository<JsonSchema>,\r\n private readonly virtualColumnService: VirtualColumnService,\r\n private readonly uiSchemaService: UiSchemaService,\r\n private readonly jsonSecurityService: JsonSecurityService\r\n ) {\r\n // กำหนดค่าเริ่มต้นให้กับ AJV Validation Engine\r\n this.ajv = new Ajv({\r\n allErrors: true, // แสดง Error ทั้งหมด ไม่หยุดแค่จุดแรก\r\n strict: false, // ไม่เคร่งครัดเกินไป (ยอมรับ Keyword แปลกๆ เช่น ui:widget)\r\n coerceTypes: true,\r\n useDefaults: true,\r\n removeAdditional: true,\r\n });\r\n addFormats(this.ajv); // เพิ่ม Format มาตรฐาน (email, date, uri ฯลฯ)\r\n this.registerCustomValidators(); // ลงทะเบียน Validator เฉพาะของโปรเจกต์\r\n }\r\n\r\n async onModuleInit() {\r\n // สามารถโหลด Schema ที่ Active ทั้งหมดมา Cache ไว้ล่วงหน้าได้ที่นี่ เพื่อความเร็วในการตอบสนองครั้งแรก\r\n }\r\n\r\n /**\r\n * ลงทะเบียน Custom Validators เฉพาะสำหรับ LCBP3\r\n */\r\n private registerCustomValidators() {\r\n // 1. ตรวจสอบรูปแบบเลขที่เอกสาร (เช่น TEAM-RFA-STR-0001)\r\n this.ajv.addFormat('document-number', {\r\n type: 'string',\r\n validate: (value: string) => {\r\n // Regex อย่างง่าย: กลุ่มตัวอักษรขีดคั่นด้วย -\r\n return /^[A-Z0-9]{2,10}-[A-Z]{2,5}(-[A-Z0-9]{2,5})?-\\d{4}-\\d{3,5}$/.test(\r\n value\r\n );\r\n },\r\n });\r\n\r\n // 2. Keyword สำหรับระบุ Role ที่จำเป็น (ใช้ร่วมกับ Security Service)\r\n this.ajv.addKeyword({\r\n keyword: 'requiredRole',\r\n type: 'string',\r\n metaSchema: { type: 'string' },\r\n validate: (_schema: string, _data: unknown) => true, // ผ่านเสมอในขั้น AJV (Security Service จะจัดการเอง)\r\n });\r\n }\r\n\r\n /**\r\n * สร้าง Schema ใหม่ พร้อมจัดการ Version, UI Schema และ Virtual Columns\r\n */\r\n async create(createDto: CreateJsonSchemaDto): Promise<JsonSchema> {\r\n // 1. ตรวจสอบความถูกต้องของ JSON Schema Definition (AJV Syntax)\r\n try {\r\n this.ajv.compile(createDto.schemaDefinition);\r\n } catch (error: unknown) {\r\n throw new BadRequestException(\r\n `Invalid JSON Schema format: ${error instanceof Error ? error.message : String(error)}`\r\n );\r\n }\r\n\r\n // 2. จัดการ UI Schema\r\n if (createDto.uiSchema) {\r\n // ถ้าส่งมา ให้ตรวจสอบความถูกต้องเทียบกับ Data Schema\r\n this.uiSchemaService.validateUiSchema(\r\n createDto.uiSchema as unknown as UiSchema,\r\n createDto.schemaDefinition\r\n );\r\n } else {\r\n // ถ้าไม่ส่งมา ให้สร้าง UI Schema พื้นฐานให้อัตโนมัติ\r\n createDto.uiSchema = this.uiSchemaService.generateDefaultUiSchema(\r\n createDto.schemaDefinition\r\n );\r\n }\r\n\r\n // 3. จัดการ Versioning อัตโนมัติ (Auto-increment)\r\n const latestSchema = await this.jsonSchemaRepository.findOne({\r\n where: { schemaCode: createDto.schemaCode },\r\n order: { version: 'DESC' },\r\n });\r\n\r\n let newVersion = 1;\r\n if (latestSchema) {\r\n // ถ้าผู้ใช้ไม่ระบุ Version หรือระบุมาน้อยกว่าล่าสุด ให้ +1\r\n if (!createDto.version || createDto.version <= latestSchema.version) {\r\n newVersion = latestSchema.version + 1;\r\n } else {\r\n newVersion = createDto.version;\r\n }\r\n } else if (createDto.version) {\r\n newVersion = createDto.version;\r\n }\r\n\r\n // 4. บันทึกลงฐานข้อมูล\r\n const newSchema = this.jsonSchemaRepository.create({\r\n ...createDto,\r\n version: newVersion,\r\n });\r\n\r\n const savedSchema = await this.jsonSchemaRepository.save(newSchema);\r\n\r\n // ล้าง Cache เพื่อให้โหลดตัวใหม่ในครั้งถัดไป\r\n this.validators.delete(savedSchema.schemaCode);\r\n\r\n this.logger.log(\r\n `Schema '${savedSchema.schemaCode}' created (v${savedSchema.version})`\r\n );\r\n\r\n // 5. สร้าง/อัปเดต Virtual Columns บน Database จริง (Performance Optimization)\r\n // Fix TS2345: Add empty array fallback\r\n if (savedSchema.virtualColumns && savedSchema.virtualColumns.length > 0) {\r\n await this.virtualColumnService.setupVirtualColumns(\r\n savedSchema.tableName,\r\n savedSchema.virtualColumns || []\r\n );\r\n }\r\n\r\n return savedSchema;\r\n }\r\n\r\n /**\r\n * ค้นหา Schema ทั้งหมด (Pagination & Filter)\r\n */\r\n async findAll(searchDto: SearchJsonSchemaDto) {\r\n const { search, isActive, page = 1, limit = 20 } = searchDto;\r\n const skip = (page - 1) * limit;\r\n\r\n const query = this.jsonSchemaRepository.createQueryBuilder('schema');\r\n\r\n if (search) {\r\n query.andWhere('schema.schemaCode LIKE :search', {\r\n search: `%${search}%`,\r\n });\r\n }\r\n\r\n if (isActive !== undefined) {\r\n query.andWhere('schema.isActive = :isActive', { isActive });\r\n }\r\n\r\n // เรียงตาม Code ก่อน แล้วตามด้วย Version ล่าสุด\r\n query.orderBy('schema.schemaCode', 'ASC');\r\n query.addOrderBy('schema.version', 'DESC');\r\n\r\n const [items, total] = await query.skip(skip).take(limit).getManyAndCount();\r\n\r\n return {\r\n data: items,\r\n meta: {\r\n total,\r\n page,\r\n limit,\r\n totalPages: Math.ceil(total / limit),\r\n },\r\n };\r\n }\r\n\r\n /**\r\n * ดึงข้อมูล Schema ตาม ID\r\n */\r\n async findOne(id: number): Promise<JsonSchema> {\r\n const schema = await this.jsonSchemaRepository.findOne({ where: { id } });\r\n if (!schema) {\r\n throw new NotFoundException(`JsonSchema with ID ${id} not found`);\r\n }\r\n return schema;\r\n }\r\n\r\n /**\r\n * ดึงข้อมูล Schema ตาม Code และ Version (สำหรับ Migration)\r\n */\r\n async findOneByCodeAndVersion(\r\n code: string,\r\n version: number\r\n ): Promise<JsonSchema> {\r\n const schema = await this.jsonSchemaRepository.findOne({\r\n where: { schemaCode: code, version },\r\n });\r\n\r\n if (!schema) {\r\n throw new NotFoundException(\r\n `JsonSchema '${code}' version ${version} not found`\r\n );\r\n }\r\n return schema;\r\n }\r\n\r\n /**\r\n * ดึง Schema เวอร์ชันล่าสุดที่ Active (สำหรับใช้งานทั่วไป)\r\n */\r\n async findLatestByCode(code: string): Promise<JsonSchema> {\r\n const schema = await this.jsonSchemaRepository.findOne({\r\n where: { schemaCode: code, isActive: true },\r\n order: { version: 'DESC' },\r\n });\r\n\r\n if (!schema) {\r\n throw new NotFoundException(\r\n `Active JsonSchema with code '${code}' not found`\r\n );\r\n }\r\n return schema;\r\n }\r\n\r\n /**\r\n * [CORE FUNCTION] ตรวจสอบข้อมูล (Validate), ทำความสะอาด (Sanitize) และเข้ารหัส (Encrypt)\r\n * ใช้สำหรับ \"ขาเข้า\" (Write) ก่อนบันทึกลง Database\r\n */\r\n async validateData(\r\n schemaCode: string,\r\n data: Record<string, unknown>,\r\n _options: ValidationOptions = {}\r\n ): Promise<ValidationResult> {\r\n // 1. ดึงและ Compile Validator\r\n const validate = await this.getValidator(schemaCode);\r\n const schema = await this.findLatestByCode(schemaCode); // ดึง Full Schema เพื่อใช้ Config อื่นๆ\r\n\r\n // 2. สำเนาข้อมูลเพื่อป้องกัน Side Effect และเตรียมสำหรับ AJV Mutation (Sanitization)\r\n const dataToValidate: Record<string, unknown> = JSON.parse(\r\n JSON.stringify(data)\r\n );\r\n\r\n // 3. เริ่มการตรวจสอบ (AJV จะทำการ Coerce Type และ Remove Additional Properties ให้ด้วย)\r\n const valid = validate(dataToValidate);\r\n\r\n // 4. จัดการกรณีข้อมูลไม่ถูกต้อง\r\n if (!valid) {\r\n const errors: ValidationErrorDetail[] = (validate.errors || []).map(\r\n (err) => ({\r\n field: err.instancePath || 'root',\r\n message: err.message || 'Validation error',\r\n value: err.params,\r\n })\r\n );\r\n\r\n return {\r\n isValid: false,\r\n errors,\r\n sanitizedData: null,\r\n };\r\n }\r\n\r\n // 5. เข้ารหัสข้อมูล (Encryption) สำหรับ Field ที่มีความลับ (x-encrypt: true)\r\n const secureData = this.jsonSecurityService.encryptFields(\r\n dataToValidate,\r\n schema.schemaDefinition\r\n );\r\n\r\n return {\r\n isValid: true,\r\n errors: [],\r\n sanitizedData: secureData, // ข้อมูลนี้สะอาดและปลอดภัย พร้อมบันทึก\r\n };\r\n }\r\n\r\n /**\r\n * [CORE FUNCTION] อ่านข้อมูล, ถอดรหัส (Decrypt) และกรองตามสิทธิ์ (Filter)\r\n * ใช้สำหรับ \"ขาออก\" (Read) ก่อนส่งให้ Frontend\r\n */\r\n async processReadData(\r\n schemaCode: string,\r\n data: Record<string, unknown>,\r\n userContext: SecurityContext\r\n ): Promise<Record<string, unknown>> {\r\n if (!data) return data;\r\n\r\n // ดึง Schema เพื่อดู Config การถอดรหัสและการมองเห็น\r\n const schema = await this.findLatestByCode(schemaCode);\r\n\r\n return this.jsonSecurityService.decryptAndFilterFields(\r\n data,\r\n schema.schemaDefinition,\r\n userContext\r\n );\r\n }\r\n\r\n /**\r\n * Helper: ดึงและ Cache AJV Validator Function เพื่อประสิทธิภาพ\r\n */\r\n private async getValidator(schemaCode: string): Promise<ValidateFunction> {\r\n let validate = this.validators.get(schemaCode);\r\n\r\n if (!validate) {\r\n const schema = await this.findLatestByCode(schemaCode);\r\n try {\r\n validate = this.ajv.compile(schema.schemaDefinition);\r\n this.validators.set(schemaCode, validate);\r\n } catch (error: unknown) {\r\n throw new BadRequestException(\r\n `Invalid Schema Definition for '${schemaCode}': ${error instanceof Error ? error.message : String(error)}`\r\n );\r\n }\r\n }\r\n return validate;\r\n }\r\n\r\n /**\r\n * Wrapper เก่าสำหรับ Backward Compatibility (ถ้ามีโค้ดเก่าเรียกใช้)\r\n */\r\n async validate(\r\n schemaCode: string,\r\n data: Record<string, unknown>\r\n ): Promise<boolean> {\r\n const result = await this.validateData(schemaCode, data);\r\n if (!result.isValid) {\r\n const errorMsg = result.errors\r\n .map((e) => `${e.field}: ${e.message}`)\r\n .join(', ');\r\n throw new BadRequestException(`JSON Validation Failed: ${errorMsg}`);\r\n }\r\n return true;\r\n }\r\n\r\n /**\r\n * อัปเดตข้อมูล Schema และจัดการผลกระทบ (Virtual Columns / UI Schema)\r\n */\r\n async update(\r\n id: number,\r\n updateDto: UpdateJsonSchemaDto\r\n ): Promise<JsonSchema> {\r\n const schema = await this.findOne(id);\r\n\r\n // ตรวจสอบ JSON Schema\r\n if (updateDto.schemaDefinition) {\r\n try {\r\n this.ajv.compile(updateDto.schemaDefinition);\r\n } catch (error: unknown) {\r\n throw new BadRequestException(\r\n `Invalid JSON Schema format: ${error instanceof Error ? error.message : String(error)}`\r\n );\r\n }\r\n this.validators.delete(schema.schemaCode); // เคลียร์ Cache เก่า\r\n }\r\n\r\n // ตรวจสอบ UI Schema\r\n if (updateDto.uiSchema) {\r\n this.uiSchemaService.validateUiSchema(\r\n updateDto.uiSchema as unknown as UiSchema,\r\n updateDto.schemaDefinition || schema.schemaDefinition\r\n );\r\n }\r\n\r\n const updatedSchema = this.jsonSchemaRepository.merge(schema, updateDto);\r\n const savedSchema = await this.jsonSchemaRepository.save(updatedSchema);\r\n\r\n // อัปเดต Virtual Columns ใน Database ถ้ามีการเปลี่ยนแปลง Config\r\n // Fix TS2345: Add empty array fallback\r\n if (updateDto.virtualColumns && updatedSchema.virtualColumns) {\r\n await this.virtualColumnService.setupVirtualColumns(\r\n savedSchema.tableName,\r\n savedSchema.virtualColumns || []\r\n );\r\n }\r\n\r\n return savedSchema;\r\n }\r\n\r\n /**\r\n * ลบ Schema (Hard Delete)\r\n */\r\n async remove(id: number): Promise<void> {\r\n const schema = await this.findOne(id);\r\n this.validators.delete(schema.schemaCode);\r\n await this.jsonSchemaRepository.remove(schema);\r\n }\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\services\\json-security.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\services\\schema-migration.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\services\\ui-schema.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\json-schema\\services\\virtual-column.service.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-unsafe-assignment",
"severity": 2,
"message": "Unsafe assignment of an `any` value.",
"line": 101,
"column": 11,
"nodeType": "VariableDeclarator",
"messageId": "anyAssignment",
"endLine": 104,
"endColumn": 7
},
{
"ruleId": "@typescript-eslint/no-unsafe-member-access",
"severity": 2,
"message": "Unsafe member access [0] on an `any` value.",
"line": 106,
"column": 16,
"nodeType": "Literal",
"messageId": "unsafeMemberExpression",
"endLine": 106,
"endColumn": 17
}
],
"suppressedMessages": [],
"errorCount": 2,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "// File: src/modules/json-schema/services/virtual-column.service.ts\r\nimport { Injectable, Logger } from '@nestjs/common';\r\nimport { DataSource, QueryRunner } from 'typeorm';\r\nimport { VirtualColumnConfig } from '../entities/json-schema.entity';\r\n\r\n@Injectable()\r\nexport class VirtualColumnService {\r\n private readonly logger = new Logger(VirtualColumnService.name);\r\n\r\n constructor(private readonly dataSource: DataSource) {}\r\n\r\n /**\r\n * สร้าง/อัปเดต Virtual Columns และ Index บน Database จริง\r\n */\r\n async setupVirtualColumns(tableName: string, configs: VirtualColumnConfig[]) {\r\n if (!configs || configs.length === 0) return;\r\n\r\n // ใช้ QueryRunner เพื่อให้จัดการ Transaction หรือ Connection ได้ละเอียด\r\n const queryRunner = this.dataSource.createQueryRunner();\r\n await queryRunner.connect();\r\n\r\n try {\r\n this.logger.log(\r\n `Start setting up virtual columns for table '${tableName}'...`\r\n );\r\n\r\n // 1. ตรวจสอบว่าตารางมีอยู่จริงไหม\r\n const tableExists = await queryRunner.hasTable(tableName);\r\n if (!tableExists) {\r\n this.logger.warn(\r\n `Table '${tableName}' not found. Skipping virtual columns.`\r\n );\r\n return;\r\n }\r\n\r\n for (const config of configs) {\r\n await this.ensureVirtualColumn(queryRunner, tableName, config);\r\n\r\n if (config.index_type) {\r\n await this.ensureIndex(queryRunner, tableName, config);\r\n }\r\n }\r\n\r\n this.logger.log(\r\n `Finished setting up virtual columns for '${tableName}'.`\r\n );\r\n } catch (err: unknown) {\r\n this.logger.error(\r\n `Failed to setup virtual columns: ${err instanceof Error ? err.message : String(err)}`,\r\n err instanceof Error ? err.stack : undefined\r\n );\r\n throw err;\r\n } finally {\r\n await queryRunner.release();\r\n }\r\n }\r\n\r\n /**\r\n * สร้าง Column ถ้ายังไม่มี\r\n */\r\n private async ensureVirtualColumn(\r\n queryRunner: QueryRunner,\r\n tableName: string,\r\n config: VirtualColumnConfig\r\n ) {\r\n const hasColumn = await queryRunner.hasColumn(\r\n tableName,\r\n config.column_name\r\n );\r\n\r\n if (!hasColumn) {\r\n const sql = this.generateAddColumnSql(tableName, config);\r\n this.logger.log(`Executing: ${sql}`);\r\n await queryRunner.query(sql);\r\n } else {\r\n // TODO: (Advance) ถ้ามี Column แล้ว แต่ Definition เปลี่ยน อาจต้อง ALTER MODIFY\r\n this.logger.debug(\r\n `Column '${config.column_name}' already exists in '${tableName}'.`\r\n );\r\n }\r\n }\r\n\r\n /**\r\n * สร้าง Index ถ้ายังไม่มี\r\n */\r\n private async ensureIndex(\r\n queryRunner: QueryRunner,\r\n tableName: string,\r\n config: VirtualColumnConfig\r\n ) {\r\n const indexName = `idx_${tableName}_${config.column_name}`;\r\n\r\n // ตรวจสอบว่า Index มีอยู่จริงไหม (Query จาก information_schema เพื่อความชัวร์)\r\n const checkIndexSql = `\r\n SELECT COUNT(1) as count\r\n FROM information_schema.STATISTICS\r\n WHERE table_schema = DATABASE()\r\n AND table_name = ?\r\n AND index_name = ?\r\n `;\r\n const result = await queryRunner.query(checkIndexSql, [\r\n tableName,\r\n indexName,\r\n ]);\r\n\r\n if (result[0].count == 0) {\r\n const sql = `CREATE ${config.index_type === 'UNIQUE' ? 'UNIQUE' : ''} INDEX ${indexName} ON ${tableName} (${config.column_name})`;\r\n this.logger.log(`Creating Index: ${sql}`);\r\n await queryRunner.query(sql);\r\n }\r\n }\r\n\r\n /**\r\n * Generate SQL สำหรับ MariaDB 10.11 Virtual Column\r\n * Syntax: ADD COLUMN name type GENERATED ALWAYS AS (expr) VIRTUAL\r\n */\r\n private generateAddColumnSql(\r\n tableName: string,\r\n config: VirtualColumnConfig\r\n ): string {\r\n const dbType = this.mapDataTypeToSql(config.data_type);\r\n // JSON_UNQUOTE(JSON_EXTRACT(details, '$.path'))\r\n // ใช้ 'details' เป็นชื่อ column JSON หลัก (ต้องตรงกับ Database Schema ที่ออกแบบไว้)\r\n const expression = `JSON_UNQUOTE(JSON_EXTRACT(details, '${config.json_path}'))`;\r\n\r\n // Handle Type Casting inside expression if needed,\r\n // but usually MariaDB handles string return from JSON_EXTRACT.\r\n // For INT/DATE, virtual column type definition enforces it.\r\n\r\n return `ALTER TABLE ${tableName} ADD COLUMN ${config.column_name} ${dbType} GENERATED ALWAYS AS (${expression}) VIRTUAL`;\r\n }\r\n\r\n private mapDataTypeToSql(type: string): string {\r\n switch (type) {\r\n case 'INT':\r\n return 'INT';\r\n case 'VARCHAR':\r\n return 'VARCHAR(255)';\r\n case 'BOOLEAN':\r\n return 'TINYINT(1)';\r\n case 'DATE':\r\n return 'DATE';\r\n case 'DATETIME':\r\n return 'DATETIME';\r\n case 'DECIMAL':\r\n return 'DECIMAL(10,2)';\r\n default:\r\n return 'VARCHAR(255)';\r\n }\r\n }\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\dto\\create-discipline.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\dto\\create-sub-type.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\dto\\create-tag.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\dto\\save-number-format.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\dto\\search-tag.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\dto\\update-tag.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\entities\\discipline.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\entities\\tag.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\master.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\master.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\master\\master.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\dto\\commit-batch.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\dto\\create-migration-error.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\dto\\enqueue-migration.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\dto\\import-correspondence.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\dto\\migration-queue-query.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\entities\\import-transaction.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\entities\\migration-error.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\entities\\migration-review-queue.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\migration.controller.spec.ts",
"messages": [
{
"ruleId": "@typescript-eslint/unbound-method",
"severity": 2,
"message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.",
"line": 53,
"column": 12,
"nodeType": "MemberExpression",
"messageId": "unboundWithoutThisAnnotation",
"endLine": 53,
"endColumn": 40
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import { Test, TestingModule } from '@nestjs/testing';\r\nimport { MigrationController } from './migration.controller';\r\nimport { MigrationService } from './migration.service';\r\nimport { ImportCorrespondenceDto } from './dto/import-correspondence.dto';\r\n\r\ndescribe('MigrationController', () => {\r\n let controller: MigrationController;\r\n let service: MigrationService;\r\n\r\n beforeEach(async () => {\r\n const module: TestingModule = await Test.createTestingModule({\r\n controllers: [MigrationController],\r\n providers: [\r\n {\r\n provide: MigrationService,\r\n useValue: {\r\n importCorrespondence: jest\r\n .fn()\r\n .mockResolvedValue({ message: 'Success' }),\r\n },\r\n },\r\n ],\r\n }).compile();\r\n\r\n controller = module.get<MigrationController>(MigrationController);\r\n service = module.get<MigrationService>(MigrationService);\r\n });\r\n\r\n it('should be defined', () => {\r\n expect(controller).toBeDefined();\r\n });\r\n\r\n it('should call importCorrespondence on service', async () => {\r\n const dto: ImportCorrespondenceDto = {\r\n document_number: 'DOC-001',\r\n subject: 'Legacy Record',\r\n category: 'Correspondence',\r\n source_file_path: '/staging_ai/test.pdf',\r\n migrated_by: 'SYSTEM_IMPORT',\r\n batch_id: 'batch1',\r\n project_id: 1,\r\n };\r\n\r\n const idempotencyKey = 'key123';\r\n const user = { userId: 5 };\r\n\r\n const result = await controller.importCorrespondence(\r\n dto,\r\n idempotencyKey,\r\n user\r\n );\r\n expect(result).toEqual({ message: 'Success' });\r\n expect(service.importCorrespondence).toHaveBeenCalledWith(\r\n dto,\r\n idempotencyKey,\r\n 5\r\n );\r\n });\r\n});\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\migration.controller.ts",
"messages": [
{
"ruleId": "@typescript-eslint/require-await",
"severity": 2,
"message": "Async method 'getStagingFile' has no 'await' expression.",
"line": 166,
"column": 3,
"nodeType": "FunctionExpression",
"messageId": "missingAwait",
"endLine": 166,
"endColumn": 23,
"suggestions": [
{ "messageId": "removeAsync", "fix": { "range": [5026, 5032], "text": "" }, "desc": "Remove 'async'." }
]
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import {\r\n Controller,\r\n Post,\r\n Body,\r\n Headers,\r\n UseGuards,\r\n Get,\r\n Param,\r\n Query,\r\n Res,\r\n ParseIntPipe,\r\n} from '@nestjs/common';\r\nimport { MigrationService } from './migration.service';\r\nimport { ImportCorrespondenceDto } from './dto/import-correspondence.dto';\r\nimport { EnqueueMigrationDto } from './dto/enqueue-migration.dto';\r\nimport { CommitBatchDto } from './dto/commit-batch.dto';\r\nimport { CreateMigrationErrorDto } from './dto/create-migration-error.dto';\r\nimport { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';\r\nimport { CurrentUser } from '../../common/decorators/current-user.decorator';\r\nimport { User } from '../user/entities/user.entity';\r\nimport {\r\n ApiTags,\r\n ApiOperation,\r\n ApiBearerAuth,\r\n ApiHeader,\r\n ApiQuery,\r\n ApiParam,\r\n} from '@nestjs/swagger';\r\nimport { MigrationQueueQueryDto } from './dto/migration-queue-query.dto';\r\nimport type { Response } from 'express';\r\n\r\n@ApiTags('Migration')\r\n@ApiBearerAuth()\r\n@Controller('migration')\r\nexport class MigrationController {\r\n constructor(private readonly migrationService: MigrationService) {}\r\n\r\n @Post('import')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({\r\n summary: 'Import generic legacy correspondence record via n8n integration',\r\n })\r\n @ApiHeader({\r\n name: 'Idempotency-Key',\r\n description:\r\n 'Unique key per document and batch to prevent duplicate inserts',\r\n required: true,\r\n })\r\n async importCorrespondence(\r\n @Body() dto: ImportCorrespondenceDto,\r\n @Headers('idempotency-key') idempotencyKey: string,\r\n @CurrentUser() user: User\r\n ) {\r\n const userId = user?.user_id || 5;\r\n return this.migrationService.importCorrespondence(\r\n dto,\r\n idempotencyKey,\r\n userId\r\n );\r\n }\r\n\r\n @Post('commit_batch')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({\r\n summary: 'Batch approve and import migration review queue items',\r\n })\r\n @ApiHeader({\r\n name: 'Idempotency-Key',\r\n description:\r\n 'Unique key for the entire batch to prevent duplicate execution',\r\n required: true,\r\n })\r\n async commitBatch(\r\n @Body() dto: CommitBatchDto,\r\n @Headers('idempotency-key') idempotencyKey: string,\r\n @CurrentUser() user: User\r\n ) {\r\n const userId = user?.user_id || 5;\r\n return this.migrationService.commitBatch(dto, idempotencyKey, userId);\r\n }\r\n\r\n @Post('queue')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({\r\n summary: 'Enqueue a record into the staging migration review queue',\r\n })\r\n async enqueueRecord(@Body() dto: EnqueueMigrationDto) {\r\n return this.migrationService.enqueueRecord(dto);\r\n }\r\n\r\n @Get('queue')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({ summary: 'Get migration review queue' })\r\n async getReviewQueue(@Query() query: MigrationQueueQueryDto) {\r\n return this.migrationService.getReviewQueue(query);\r\n }\r\n\r\n @Get('queue/:id')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({ summary: 'Get a specific queue item by ID' })\r\n @ApiParam({ name: 'id', type: Number })\r\n async getQueueItemById(@Param('id', ParseIntPipe) id: number) {\r\n return this.migrationService.getQueueItemById(id);\r\n }\r\n\r\n @Post('errors')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({ summary: 'Log a migration error from n8n workflow' })\r\n async createError(@Body() dto: CreateMigrationErrorDto) {\r\n return this.migrationService.createError(dto);\r\n }\r\n\r\n @Get('errors')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({ summary: 'Get migration errors' })\r\n @ApiQuery({ name: 'page', required: false, type: Number })\r\n @ApiQuery({ name: 'limit', required: false, type: Number })\r\n async getErrors(\r\n @Query('page') page?: number,\r\n @Query('limit') limit?: number\r\n ) {\r\n return this.migrationService.getErrors(page, limit);\r\n }\r\n\r\n @Post('queue/:id/approve')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({ summary: 'Approve and import a queued migration item' })\r\n @ApiParam({ name: 'id', type: Number })\r\n @ApiHeader({\r\n name: 'Idempotency-Key',\r\n description:\r\n 'Unique key per document and batch to prevent duplicate inserts',\r\n required: true,\r\n })\r\n async approveQueueItem(\r\n @Param('id', ParseIntPipe) id: number,\r\n @Body() dto: ImportCorrespondenceDto,\r\n @Headers('idempotency-key') idempotencyKey: string,\r\n @CurrentUser() user: User\r\n ) {\r\n const userId = user?.user_id || 5;\r\n return this.migrationService.approveQueueItem(\r\n id,\r\n dto,\r\n idempotencyKey,\r\n userId\r\n );\r\n }\r\n\r\n @Post('queue/:id/reject')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({ summary: 'Reject a queued migration item' })\r\n @ApiParam({ name: 'id', type: Number })\r\n async rejectQueueItem(\r\n @Param('id', ParseIntPipe) id: number,\r\n @CurrentUser() user: User\r\n ) {\r\n const userId = user?.user_id || 5;\r\n return this.migrationService.rejectQueueItem(id, userId);\r\n }\r\n\r\n @Get('staging-file')\r\n @UseGuards(JwtAuthGuard)\r\n @ApiOperation({ summary: 'Stream a file from staging' })\r\n @ApiQuery({ name: 'path', required: true, type: String })\r\n async getStagingFile(@Query('path') filePath: string, @Res() res: Response) {\r\n const stream = this.migrationService.getStagingFileStream(filePath);\r\n res.set({\r\n 'Content-Type': 'application/pdf',\r\n 'Content-Disposition': 'inline; filename=\"document.pdf\"',\r\n });\r\n stream.pipe(res);\r\n }\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\migration.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\migration.service.spec.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\migration\\migration.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\monitoring\\controllers\\health.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\monitoring\\dto\\set-maintenance.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\monitoring\\logger\\winston.config.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\monitoring\\monitoring.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\monitoring\\monitoring.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\monitoring\\monitoring.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\monitoring\\services\\metrics.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\notification\\dto\\create-notification.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\notification\\dto\\search-notification.dto.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-unsafe-return",
"severity": 2,
"message": "Unsafe return of a value of type `any`.",
"line": 20,
"column": 5,
"nodeType": "ReturnStatement",
"messageId": "unsafeReturn",
"endLine": 20,
"endColumn": 18
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import { IsInt, IsOptional, IsBoolean } from 'class-validator';\r\nimport { Type, Transform } from 'class-transformer';\r\n\r\nexport class SearchNotificationDto {\r\n @IsOptional()\r\n @IsInt()\r\n @Type(() => Number)\r\n page: number = 1;\r\n\r\n @IsOptional()\r\n @IsInt()\r\n @Type(() => Number)\r\n limit: number = 20;\r\n\r\n @IsOptional()\r\n @IsBoolean()\r\n @Transform(({ value }) => {\r\n if (value === 'true') return true;\r\n if (value === 'false') return false;\r\n return value;\r\n })\r\n isRead?: boolean; // กรอง: อ่านแล้ว/ยังไม่อ่าน\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\notification\\entities\\notification.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\notification\\notification-cleanup.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\notification\\notification.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\notification\\notification.gateway.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\notification\\notification.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\notification\\notification.processor.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\notification\\notification.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\organization\\dto\\create-organization.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\organization\\dto\\search-organization.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\organization\\dto\\update-organization.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\organization\\entities\\organization-role.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\organization\\entities\\organization.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\organization\\organization.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\organization\\organization.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\organization\\organization.service.ts",
"messages": [
{
"ruleId": "prettier/prettier",
"severity": 2,
"message": "Insert `····`",
"line": 76,
"column": 1,
"nodeType": null,
"messageId": "insert",
"endLine": 76,
"endColumn": 1,
"fix": { "range": [2499, 2499], "text": " " }
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 1,
"fixableWarningCount": 0,
"source": "import {\r\n Injectable,\r\n NotFoundException,\r\n ConflictException,\r\n} from '@nestjs/common';\r\nimport { InjectRepository } from '@nestjs/typeorm';\r\nimport { Repository } from 'typeorm';\r\nimport { Organization } from './entities/organization.entity';\r\nimport { CreateOrganizationDto } from './dto/create-organization.dto.js';\r\nimport { UpdateOrganizationDto } from './dto/update-organization.dto.js';\r\n\r\n@Injectable()\r\nexport class OrganizationService {\r\n constructor(\r\n @InjectRepository(Organization)\r\n private readonly orgRepo: Repository<Organization>\r\n ) {}\r\n\r\n async create(dto: CreateOrganizationDto) {\r\n const existing = await this.orgRepo.findOne({\r\n where: { organizationCode: dto.organizationCode },\r\n });\r\n if (existing) {\r\n throw new ConflictException(\r\n `Organization Code \"${dto.organizationCode}\" already exists`\r\n );\r\n }\r\n const org = this.orgRepo.create(dto);\r\n return this.orgRepo.save(org);\r\n }\r\n\r\n async findAll(params?: {\r\n search?: string;\r\n roleId?: number;\r\n projectId?: number;\r\n page?: number;\r\n limit?: number;\r\n }) {\r\n const { search, roleId, projectId, page = 1, limit = 100 } = params || {};\r\n const skip = (page - 1) * limit;\r\n\r\n // Start with a basic query builder to handle dynamic conditions easily\r\n const queryBuilder = this.orgRepo.createQueryBuilder('org');\r\n\r\n if (search) {\r\n queryBuilder.andWhere(\r\n '(org.organizationCode LIKE :search OR org.organizationName LIKE :search)',\r\n { search: `%${search}%` }\r\n );\r\n }\r\n\r\n // [Refactor] Support filtering by roleId (e.g., getting all CONTRACTORS)\r\n if (roleId) {\r\n // Assuming there is a relation or a way to filter by role.\r\n // If Organization has a roleId column directly:\r\n queryBuilder.andWhere('org.roleId = :roleId', { roleId });\r\n }\r\n\r\n // [New] Support filtering by projectId (e.g. organizations in a project)\r\n // Assuming a Many-to-Many or One-to-Many relation exists via ProjectOrganization\r\n if (projectId) {\r\n // Use raw join to avoid circular dependency with ProjectOrganization entity\r\n queryBuilder.innerJoin(\r\n 'project_organizations',\r\n 'po',\r\n 'po.organization_id = org.id AND po.project_id = :projectId',\r\n { projectId }\r\n );\r\n }\r\n\r\n queryBuilder.orderBy('org.organizationCode', 'ASC').skip(skip).take(limit);\r\n\r\n const [data, total] = await queryBuilder.getManyAndCount();\r\n\r\n // Debug logging\r\n// console.log(`[OrganizationService] Found ${total} organizations`);\r\n\r\n return {\r\n data,\r\n meta: {\r\n total,\r\n page,\r\n limit,\r\n totalPages: Math.ceil(total / limit),\r\n },\r\n };\r\n }\r\n\r\n async findOne(id: number) {\r\n const org = await this.orgRepo.findOne({ where: { id } });\r\n if (!org) throw new NotFoundException(`Organization ID ${id} not found`);\r\n return org;\r\n }\r\n\r\n async findOneByUuid(uuid: string) {\r\n const org = await this.orgRepo.findOne({ where: { uuid } });\r\n if (!org)\r\n throw new NotFoundException(`Organization UUID ${uuid} not found`);\r\n return org;\r\n }\r\n\r\n async update(uuid: string, dto: UpdateOrganizationDto) {\r\n const org = await this.findOneByUuid(uuid);\r\n Object.assign(org, dto);\r\n return this.orgRepo.save(org);\r\n }\r\n\r\n async remove(uuid: string) {\r\n const org = await this.findOneByUuid(uuid);\r\n return this.orgRepo.remove(org);\r\n }\r\n\r\n async findAllActive() {\r\n return this.orgRepo.find({\r\n where: { isActive: true },\r\n order: { organizationCode: 'ASC' },\r\n });\r\n }\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\dto\\create-project.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\dto\\search-project.dto.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-unsafe-return",
"severity": 2,
"message": "Unsafe return of a value of type `any`.",
"line": 14,
"column": 5,
"nodeType": "ReturnStatement",
"messageId": "unsafeReturn",
"endLine": 14,
"endColumn": 18
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import { IsString, IsOptional, IsInt, IsBoolean } from 'class-validator';\r\nimport { Type, Transform } from 'class-transformer';\r\n\r\nexport class SearchProjectDto {\r\n @IsString()\r\n @IsOptional()\r\n search?: string; // ค้นหาจาก Project Code หรือ Name\r\n\r\n @IsOptional()\r\n @IsBoolean()\r\n @Transform(({ value }) => {\r\n if (value === 'true') return true;\r\n if (value === 'false') return false;\r\n return value;\r\n })\r\n isActive?: boolean; // กรองตามสถานะ Active\r\n\r\n @IsOptional()\r\n @IsInt()\r\n @Type(() => Number)\r\n page: number = 1;\r\n\r\n @IsOptional()\r\n @IsInt()\r\n @Type(() => Number)\r\n limit: number = 20;\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\dto\\update-project.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\entities\\project-organization.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\entities\\project.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\project.controller.spec.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\project.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\project.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\project.service.spec.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-unsafe-call",
"severity": 2,
"message": "Unsafe call of an `any` typed value.",
"line": 64,
"column": 7,
"nodeType": "MemberExpression",
"messageId": "unsafeCall",
"endLine": 66,
"endColumn": 43
},
{
"ruleId": "@typescript-eslint/no-unsafe-member-access",
"severity": 2,
"message": "Unsafe member access .getManyAndCount on an `any` value.",
"line": 66,
"column": 10,
"nodeType": "Identifier",
"messageId": "unsafeMemberExpression",
"endLine": 66,
"endColumn": 25
}
],
"suppressedMessages": [],
"errorCount": 2,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import { Test, TestingModule } from '@nestjs/testing';\r\nimport { getRepositoryToken } from '@nestjs/typeorm';\r\nimport { ProjectService } from './project.service';\r\nimport { Project } from './entities/project.entity';\r\nimport { OrganizationService } from '../organization/organization.service';\r\n\r\ndescribe('ProjectService', () => {\r\n let service: ProjectService;\r\n let mockProjectRepository: Record<string, jest.Mock>;\r\n let mockOrganizationService: Record<string, jest.Mock>;\r\n\r\n beforeEach(async () => {\r\n mockProjectRepository = {\r\n find: jest.fn(),\r\n findOne: jest.fn(),\r\n create: jest.fn(),\r\n save: jest.fn(),\r\n softDelete: jest.fn(),\r\n createQueryBuilder: jest.fn(() => ({\r\n leftJoinAndSelect: jest.fn().mockReturnThis(),\r\n where: jest.fn().mockReturnThis(),\r\n andWhere: jest.fn().mockReturnThis(),\r\n orderBy: jest.fn().mockReturnThis(),\r\n skip: jest.fn().mockReturnThis(),\r\n take: jest.fn().mockReturnThis(),\r\n getManyAndCount: jest.fn().mockResolvedValue([[], 0]),\r\n })),\r\n };\r\n\r\n mockOrganizationService = {\r\n findAllActive: jest.fn(),\r\n };\r\n\r\n const module: TestingModule = await Test.createTestingModule({\r\n providers: [\r\n ProjectService,\r\n {\r\n provide: getRepositoryToken(Project),\r\n useValue: mockProjectRepository,\r\n },\r\n {\r\n provide: OrganizationService,\r\n useValue: mockOrganizationService,\r\n },\r\n ],\r\n }).compile();\r\n\r\n service = module.get<ProjectService>(ProjectService);\r\n });\r\n\r\n it('should be defined', () => {\r\n expect(service).toBeDefined();\r\n });\r\n\r\n describe('findAll', () => {\r\n it('should return paginated projects', async () => {\r\n const mockProjects = [\r\n {\r\n project_id: 1,\r\n project_code: 'PROJ-001',\r\n project_name: 'Test Project',\r\n },\r\n ];\r\n mockProjectRepository\r\n .createQueryBuilder()\r\n .getManyAndCount.mockResolvedValue([mockProjects, 1]);\r\n\r\n const result = await service.findAll({ page: 1, limit: 10 });\r\n\r\n expect(result.data).toBeDefined();\r\n expect(result.meta).toBeDefined();\r\n });\r\n });\r\n\r\n describe('findAllOrganizations', () => {\r\n it('should return all organizations', async () => {\r\n const mockOrgs = [{ organization_id: 1, name: 'Test Org' }];\r\n mockOrganizationService.findAllActive.mockResolvedValue(mockOrgs);\r\n\r\n const result = await service.findAllOrganizations();\r\n\r\n expect(mockOrganizationService.findAllActive).toHaveBeenCalled();\r\n expect(result).toEqual(mockOrgs);\r\n });\r\n });\r\n});\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\project\\project.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\dto\\create-rfa-revision.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\dto\\create-rfa-workflow.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\dto\\create-rfa.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\dto\\search-rfa.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\dto\\submit-rfa.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\dto\\update-rfa.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\entities\\rfa-approve-code.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\entities\\rfa-item.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\entities\\rfa-revision.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\entities\\rfa-status-code.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\entities\\rfa-type.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\entities\\rfa-workflow-template-step.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\entities\\rfa-workflow-template.entity.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 26,
"column": 35,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 26,
"endColumn": 38,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [638, 641], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [638, 641], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import {\r\n Entity,\r\n PrimaryGeneratedColumn,\r\n Column,\r\n CreateDateColumn,\r\n UpdateDateColumn,\r\n OneToMany,\r\n} from 'typeorm';\r\nimport { RfaWorkflowTemplateStep } from './rfa-workflow-template-step.entity';\r\n\r\n@Entity('rfa_workflow_templates')\r\nexport class RfaWorkflowTemplate {\r\n @PrimaryGeneratedColumn()\r\n id!: number;\r\n\r\n @Column({ name: 'template_name', length: 100 })\r\n templateName!: string;\r\n\r\n @Column({ type: 'text', nullable: true })\r\n description?: string;\r\n\r\n @Column({ name: 'is_active', default: true })\r\n isActive!: boolean;\r\n\r\n @Column({ type: 'json', nullable: true })\r\n workflowConfig?: Record<string, any>; // Configuration เพิ่มเติม\r\n\r\n @CreateDateColumn({ name: 'created_at' })\r\n createdAt!: Date;\r\n\r\n @UpdateDateColumn({ name: 'updated_at' })\r\n updatedAt!: Date;\r\n\r\n // Relations\r\n @OneToMany(() => RfaWorkflowTemplateStep, (step) => step.template, {\r\n cascade: true,\r\n })\r\n steps!: RfaWorkflowTemplateStep[];\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\entities\\rfa-workflow.entity.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 55,
"column": 33,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 55,
"endColumn": 36,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [1420, 1423], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [1420, 1423], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "// File: src/modules/rfa/entities/rfa-workflow.entity.ts\r\nimport {\r\n Column,\r\n CreateDateColumn,\r\n Entity,\r\n JoinColumn,\r\n ManyToOne,\r\n PrimaryGeneratedColumn,\r\n UpdateDateColumn,\r\n} from 'typeorm';\r\nimport { Organization } from '../../organization/entities/organization.entity';\r\nimport { User } from '../../user/entities/user.entity';\r\nimport { RfaRevision } from './rfa-revision.entity';\r\nimport { RfaActionType } from './rfa-workflow-template-step.entity'; // ✅ Import Enum\r\n\r\n@Entity('rfa_workflows')\r\nexport class RfaWorkflow {\r\n @PrimaryGeneratedColumn()\r\n id!: number;\r\n\r\n @Column({ name: 'rfa_revision_id' })\r\n rfaRevisionId!: number;\r\n\r\n @Column({ name: 'step_number' })\r\n stepNumber!: number;\r\n\r\n @Column({ name: 'organization_id' })\r\n organizationId!: number;\r\n\r\n @Column({ name: 'assigned_to', nullable: true })\r\n assignedTo?: number;\r\n\r\n @Column({\r\n name: 'action_type',\r\n type: 'enum',\r\n enum: RfaActionType, // ✅ Use Shared Enum\r\n nullable: true,\r\n })\r\n actionType?: RfaActionType;\r\n\r\n @Column({\r\n type: 'enum',\r\n enum: ['PENDING', 'IN_PROGRESS', 'COMPLETED', 'REJECTED'],\r\n nullable: true,\r\n })\r\n status?: string;\r\n\r\n @Column({ type: 'text', nullable: true })\r\n comments?: string;\r\n\r\n @Column({ name: 'completed_at', type: 'datetime', nullable: true })\r\n completedAt?: Date;\r\n\r\n @Column({ type: 'json', nullable: true })\r\n stateContext?: Record<string, any>;\r\n\r\n @CreateDateColumn({ name: 'created_at' })\r\n createdAt!: Date;\r\n\r\n @UpdateDateColumn({ name: 'updated_at' })\r\n updatedAt!: Date;\r\n\r\n // Relations\r\n @ManyToOne(() => RfaRevision, (rev) => rev.workflows, { onDelete: 'CASCADE' })\r\n @JoinColumn({ name: 'rfa_revision_id' })\r\n rfaRevision!: RfaRevision;\r\n\r\n @ManyToOne(() => Organization)\r\n @JoinColumn({ name: 'organization_id' })\r\n organization!: Organization;\r\n\r\n @ManyToOne(() => User)\r\n @JoinColumn({ name: 'assigned_to' })\r\n assignee?: User;\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\entities\\rfa.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\rfa-workflow.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\rfa.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\rfa.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\rfa\\rfa.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\search\\dto\\search-query.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\search\\search.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\search\\search.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\search\\search.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\transmittal\\dto\\create-transmittal.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\transmittal\\dto\\search-transmittal.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\transmittal\\dto\\update-transmittal.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\transmittal\\entities\\transmittal-item.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\transmittal\\entities\\transmittal.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\transmittal\\transmittal.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\transmittal\\transmittal.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\transmittal\\transmittal.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\dto\\assign-role.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\dto\\bulk-assignment.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\dto\\create-user.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\dto\\search-user.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\dto\\update-preference.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\dto\\update-user.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\entities\\permission.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\entities\\role.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\entities\\user-assignment.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\entities\\user-preference.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\entities\\user.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\user-assignment.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\user-preference.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\user.controller.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\user.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\user.service.spec.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\user\\user.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\dsl\\parser.service.spec.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\dsl\\parser.service.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-unsafe-assignment",
"severity": 2,
"message": "Unsafe assignment of an `any` value.",
"line": 24,
"column": 13,
"nodeType": "VariableDeclarator",
"messageId": "anyAssignment",
"endLine": 24,
"endColumn": 41
},
{
"ruleId": "@typescript-eslint/no-unsafe-assignment",
"severity": 2,
"message": "Unsafe assignment of an `any` value.",
"line": 185,
"column": 13,
"nodeType": "VariableDeclarator",
"messageId": "anyAssignment",
"endLine": 185,
"endColumn": 41
}
],
"suppressedMessages": [],
"errorCount": 2,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import { Injectable, Logger, BadRequestException } from '@nestjs/common';\r\nimport { InjectRepository } from '@nestjs/typeorm';\r\nimport { Repository } from 'typeorm';\r\nimport { WorkflowDslSchema, WorkflowDsl } from './workflow-dsl.schema';\r\nimport { WorkflowDefinition } from '../entities/workflow-definition.entity';\r\n\r\n@Injectable()\r\nexport class WorkflowDslParser {\r\n private readonly logger = new Logger(WorkflowDslParser.name);\r\n\r\n constructor(\r\n @InjectRepository(WorkflowDefinition)\r\n private workflowDefRepo: Repository<WorkflowDefinition>\r\n ) {}\r\n\r\n /**\r\n * Parse และ Validate Workflow DSL from JSON string\r\n * @param dslJson JSON string ของ Workflow DSL\r\n * @returns WorkflowDefinition entity พร้อมบันทึกลง database\r\n */\r\n async parse(dslJson: string): Promise<WorkflowDefinition> {\r\n try {\r\n // Step 1: Parse JSON\r\n const rawDsl = JSON.parse(dslJson);\r\n\r\n // Step 2: Validate with Zod schema\r\n const dsl = WorkflowDslSchema.parse(rawDsl);\r\n\r\n // Step 3: Validate state machine integrity\r\n this.validateStateMachine(dsl);\r\n\r\n // Step 4: Build WorkflowDefinition entity\r\n const definition = this.buildDefinition(dsl);\r\n\r\n // Step 5: Save to database\r\n return await this.workflowDefRepo.save(definition);\r\n } catch (error: unknown) {\r\n if (error instanceof SyntaxError) {\r\n throw new BadRequestException(`Invalid JSON: ${error.message}`);\r\n }\r\n const err = error as {\r\n name?: string;\r\n errors?: unknown;\r\n message?: string;\r\n };\r\n if (err.name === 'ZodError') {\r\n throw new BadRequestException(\r\n `Invalid workflow DSL: ${JSON.stringify(err.errors)}`\r\n );\r\n }\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * Validate state machine integrity\r\n * ตรวจสอบว่า state machine ถูกต้องตามหลักการ:\r\n * - All states in transitions must exist in states array\r\n * - Initial state must exist\r\n * - All final states must exist\r\n * - No dead-end states (states with no outgoing transitions except final states)\r\n */\r\n private validateStateMachine(dsl: WorkflowDsl): void {\r\n const stateSet = new Set(dsl.states);\r\n const finalStateSet = new Set(dsl.finalStates);\r\n\r\n // 1. Validate initial state\r\n if (!stateSet.has(dsl.initialState)) {\r\n throw new BadRequestException(\r\n `Initial state \"${dsl.initialState}\" not found in states array`\r\n );\r\n }\r\n\r\n // 2. Validate final states\r\n dsl.finalStates.forEach((state) => {\r\n if (!stateSet.has(state)) {\r\n throw new BadRequestException(\r\n `Final state \"${state}\" not found in states array`\r\n );\r\n }\r\n });\r\n\r\n // 3. Validate transitions\r\n const statesWithOutgoing = new Set<string>();\r\n\r\n dsl.transitions.forEach((transition, index) => {\r\n // Check 'from' state\r\n if (!stateSet.has(transition.from)) {\r\n throw new BadRequestException(\r\n `Transition ${index}: 'from' state \"${transition.from}\" not found in states array`\r\n );\r\n }\r\n\r\n // Check 'to' state\r\n if (!stateSet.has(transition.to)) {\r\n throw new BadRequestException(\r\n `Transition ${index}: 'to' state \"${transition.to}\" not found in states array`\r\n );\r\n }\r\n\r\n // Track states with outgoing transitions\r\n statesWithOutgoing.add(transition.from);\r\n });\r\n\r\n // 4. Check for dead-end states (except final states)\r\n const nonFinalStates = dsl.states.filter(\r\n (state) => !finalStateSet.has(state)\r\n );\r\n\r\n nonFinalStates.forEach((state) => {\r\n if (!statesWithOutgoing.has(state) && state !== dsl.initialState) {\r\n this.logger.warn(\r\n `Warning: State \"${state}\" has no outgoing transitions (potential dead-end)`\r\n );\r\n }\r\n });\r\n\r\n // 5. Check for duplicate transitions\r\n const transitionKeys = new Set<string>();\r\n dsl.transitions.forEach((transition) => {\r\n const key = `${transition.from}-${transition.trigger}-${transition.to}`;\r\n if (transitionKeys.has(key)) {\r\n throw new BadRequestException(\r\n `Duplicate transition: ${transition.from} --[${transition.trigger}]--> ${transition.to}`\r\n );\r\n }\r\n transitionKeys.add(key);\r\n });\r\n\r\n this.logger.log(\r\n `Workflow \"${dsl.name}\" v${dsl.version} validated successfully`\r\n );\r\n }\r\n\r\n /**\r\n * Build WorkflowDefinition entity from validated DSL\r\n */\r\n private buildDefinition(dsl: WorkflowDsl): WorkflowDefinition {\r\n const definition = new WorkflowDefinition();\r\n definition.workflow_code = dsl.name;\r\n // Map Semver (1.0.0) to version int (1)\r\n const majorVersion = Number(dsl.version.split('.')[0], 10);\r\n definition.version = isNaN(majorVersion) ? 1 : majorVersion;\r\n definition.description = dsl.description;\r\n definition.dsl = dsl;\r\n definition.compiled = dsl;\r\n definition.is_active = true;\r\n\r\n return definition;\r\n }\r\n\r\n /**\r\n * Get parsed DSL from WorkflowDefinition\r\n */\r\n async getParsedDsl(definitionId: string): Promise<WorkflowDsl> {\r\n const definition = await this.workflowDefRepo.findOne({\r\n where: { id: definitionId },\r\n });\r\n\r\n if (!definition) {\r\n throw new BadRequestException(\r\n `Workflow definition ${definitionId} not found`\r\n );\r\n }\r\n\r\n try {\r\n const dsl = definition.dsl;\r\n return WorkflowDslSchema.parse(dsl);\r\n } catch (error: unknown) {\r\n this.logger.error(\r\n `Failed to parse stored DSL for definition ${definitionId}`,\r\n error\r\n );\r\n throw new BadRequestException(\r\n `Invalid stored DSL: ${error instanceof Error ? error.message : String(error)}`\r\n );\r\n }\r\n }\r\n\r\n /**\r\n * Validate DSL without saving (dry-run)\r\n */\r\n validateOnly(dslJson: string): { valid: boolean; errors?: string[] } {\r\n try {\r\n const rawDsl = JSON.parse(dslJson);\r\n const dsl = WorkflowDslSchema.parse(rawDsl);\r\n this.validateStateMachine(dsl);\r\n return { valid: true };\r\n } catch (error: unknown) {\r\n return {\r\n valid: false,\r\n errors: [\r\n error instanceof Error ? error.message : 'Unknown validation error',\r\n ],\r\n };\r\n }\r\n }\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\dsl\\workflow-dsl.schema.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\dto\\create-workflow-definition.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\dto\\evaluate-workflow.dto.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 24,
"column": 28,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 24,
"endColumn": 31,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [777, 780], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [777, 780], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "// File: src/modules/workflow-engine/dto/evaluate-workflow.dto.ts\r\nimport { IsString, IsNotEmpty, IsObject, IsOptional } from 'class-validator';\r\nimport { ApiProperty } from '@nestjs/swagger';\r\n\r\nexport class EvaluateWorkflowDto {\r\n @ApiProperty({ example: 'RFA', description: 'รหัส Workflow' })\r\n @IsString()\r\n @IsNotEmpty()\r\n workflow_code!: string; // เพิ่ม !\r\n\r\n @ApiProperty({ example: 'DRAFT', description: 'สถานะปัจจุบัน' })\r\n @IsString()\r\n @IsNotEmpty()\r\n current_state!: string; // เพิ่ม !\r\n\r\n @ApiProperty({ example: 'SUBMIT', description: 'Action ที่ต้องการทำ' })\r\n @IsString()\r\n @IsNotEmpty()\r\n action!: string; // เพิ่ม !\r\n\r\n @ApiProperty({ description: 'Context', example: { userId: 1 } })\r\n @IsObject()\r\n @IsOptional()\r\n context?: Record<string, any>;\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\dto\\get-available-actions.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\dto\\update-workflow-definition.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\dto\\workflow-transition.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\entities\\workflow-definition.entity.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\entities\\workflow-history.entity.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 57,
"column": 29,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 57,
"endColumn": 32,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [1615, 1618], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [1615, 1618], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "// File: src/modules/workflow-engine/entities/workflow-history.entity.ts\r\n\r\nimport {\r\n Column,\r\n CreateDateColumn,\r\n Entity,\r\n Index,\r\n JoinColumn,\r\n ManyToOne,\r\n PrimaryGeneratedColumn,\r\n} from 'typeorm';\r\nimport { WorkflowInstance } from './workflow-instance.entity';\r\n\r\n/**\r\n * เก็บประวัติการเปลี่ยนสถานะ (Audit Trail)\r\n * สำคัญมากสำหรับการตรวจสอบย้อนหลัง (Who did What, When)\r\n */\r\n@Entity('workflow_histories')\r\n@Index(['instanceId']) // ค้นหาประวัติของ Instance นี้\r\n@Index(['actionByUserId']) // ค้นหาว่า User คนนี้ทำอะไรไปบ้าง\r\nexport class WorkflowHistory {\r\n @PrimaryGeneratedColumn('uuid')\r\n id!: string;\r\n\r\n @ManyToOne(() => WorkflowInstance, { onDelete: 'CASCADE' })\r\n @JoinColumn({ name: 'instance_id' })\r\n instance!: WorkflowInstance;\r\n\r\n @Column({ name: 'instance_id' })\r\n instanceId!: string;\r\n\r\n @Column({ name: 'from_state', length: 50, comment: 'สถานะต้นทาง' })\r\n fromState!: string;\r\n\r\n @Column({ name: 'to_state', length: 50, comment: 'สถานะปลายทาง' })\r\n toState!: string;\r\n\r\n @Column({ length: 50, comment: 'Action ที่ User กด (เช่น APPROVE, REJECT)' })\r\n action!: string;\r\n\r\n @Column({\r\n name: 'action_by_user_id',\r\n nullable: true,\r\n comment: 'User ID ผู้ดำเนินการ (Nullable กรณี System Auto)',\r\n })\r\n actionByUserId?: number;\r\n\r\n @Column({ type: 'text', nullable: true, comment: 'ความเห็นประกอบการอนุมัติ' })\r\n comment?: string;\r\n\r\n // Snapshot ข้อมูล ณ เวลาที่เปลี่ยนสถานะ เพื่อเป็นหลักฐานหาก Context เปลี่ยนในอนาคต\r\n @Column({\r\n type: 'json',\r\n nullable: true,\r\n comment: 'Snapshot of Context or Metadata',\r\n })\r\n metadata?: Record<string, any>;\r\n\r\n @CreateDateColumn({ name: 'created_at' })\r\n createdAt!: Date;\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\entities\\workflow-instance.entity.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 73,
"column": 28,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 73,
"endColumn": 31,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [2108, 2111], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [2108, 2111], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "// File: src/modules/workflow-engine/entities/workflow-instance.entity.ts\r\n\r\nimport {\r\n Column,\r\n CreateDateColumn,\r\n Entity,\r\n Index,\r\n JoinColumn,\r\n ManyToOne,\r\n PrimaryGeneratedColumn,\r\n UpdateDateColumn,\r\n} from 'typeorm';\r\nimport { WorkflowDefinition } from './workflow-definition.entity';\r\n\r\nexport enum WorkflowStatus {\r\n ACTIVE = 'ACTIVE', // กำลังดำเนินการ\r\n COMPLETED = 'COMPLETED', // จบกระบวนการ (ถึง Terminal State)\r\n CANCELLED = 'CANCELLED', // ถูกยกเลิกกลางคัน\r\n TERMINATED = 'TERMINATED', // ถูกบังคับจบโดยระบบ หรือ Error\r\n}\r\n\r\n/**\r\n * เก็บสถานะการเดินเรื่องของเอกสารแต่ละใบ (Runtime State)\r\n */\r\n@Entity('workflow_instances')\r\n@Index(['entityType', 'entityId']) // เพื่อค้นหาว่าเอกสารนี้ (เช่น RFA-001) อยู่ขั้นตอนไหน\r\n@Index(['currentState']) // เพื่อ Dashboard: \"มีงานค้างที่ขั้นตอนไหนบ้าง\"\r\nexport class WorkflowInstance {\r\n @PrimaryGeneratedColumn('uuid')\r\n id!: string;\r\n\r\n // ผูกกับ Definition เพื่อรู้ว่าใช้กฎชุดไหน (Version ไหน)\r\n @ManyToOne(() => WorkflowDefinition)\r\n @JoinColumn({ name: 'definition_id' })\r\n definition!: WorkflowDefinition;\r\n\r\n @Column({ name: 'definition_id' })\r\n definitionId!: string;\r\n\r\n // Polymorphic Relation: เชื่อมกับเอกสารได้หลายประเภท (RFA, CORR, etc.) โดยไม่ต้อง Foreign Key จริง\r\n @Column({\r\n name: 'entity_type',\r\n length: 50,\r\n comment: 'ประเภทเอกสาร เช่น rfa, correspondence',\r\n })\r\n entityType!: string;\r\n\r\n @Column({\r\n name: 'entity_id',\r\n length: 50,\r\n comment: 'ID ของเอกสาร (String/UUID)',\r\n })\r\n entityId!: string;\r\n\r\n @Column({\r\n name: 'current_state',\r\n length: 50,\r\n comment: 'ชื่อ State ปัจจุบัน เช่น DRAFT, IN_REVIEW',\r\n })\r\n currentState!: string;\r\n\r\n @Column({\r\n type: 'enum',\r\n enum: WorkflowStatus,\r\n default: WorkflowStatus.ACTIVE,\r\n comment: 'สถานะภาพรวมของ Instance',\r\n })\r\n status!: WorkflowStatus;\r\n\r\n // Context: เก็บตัวแปรที่จำเป็นสำหรับการตัดสินใจใน Workflow\r\n // เช่น { \"amount\": 500000, \"requester_role\": \"ENGINEER\", \"approver_ids\": [1, 2] }\r\n @Column({ type: 'json', nullable: true, comment: 'Runtime Context Data' })\r\n context?: Record<string, any>;\r\n\r\n @CreateDateColumn({ name: 'created_at' })\r\n createdAt!: Date;\r\n\r\n @UpdateDateColumn({ name: 'updated_at' })\r\n updatedAt!: Date;\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\interfaces\\workflow.interface.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\workflow-dsl.service.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-implied-eval",
"severity": 2,
"message": "Implied eval. Do not use the Function constructor to create functions.",
"line": 258,
"column": 20,
"nodeType": "NewExpression",
"messageId": "noFunctionConstructor",
"endLine": 258,
"endColumn": 68
},
{
"ruleId": "@typescript-eslint/no-unsafe-call",
"severity": 2,
"message": "Unsafe call of a `Function` typed value.",
"line": 259,
"column": 16,
"nodeType": "Identifier",
"messageId": "unsafeCall",
"endLine": 259,
"endColumn": 20
}
],
"suppressedMessages": [],
"errorCount": 2,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "// File: src/modules/workflow-engine/workflow-dsl.service.ts\r\n\r\nimport { BadRequestException, Injectable, Logger } from '@nestjs/common';\r\n\r\n// ==========================================\r\n// 1. Interfaces for RAW DSL (Input from User)\r\n// ==========================================\r\nexport interface RawWorkflowDSL {\r\n workflow: string;\r\n version?: number;\r\n description?: string;\r\n states: RawState[];\r\n}\r\n\r\nexport interface RawState {\r\n name: string;\r\n initial?: boolean;\r\n terminal?: boolean;\r\n on?: Record<string, RawTransition>;\r\n}\r\n\r\nexport interface RawTransition {\r\n to: string;\r\n require?: {\r\n role?: string | string[];\r\n user?: string;\r\n };\r\n condition?: string; // JavaScript Expression string\r\n events?: RawEvent[];\r\n}\r\n\r\nexport interface RawEvent {\r\n type: 'notify' | 'webhook' | 'assign' | 'auto_action';\r\n target?: string;\r\n template?: string;\r\n payload?: Record<string, unknown>;\r\n}\r\n\r\n// ==========================================\r\n// 2. Interfaces for COMPILED Schema (Optimized for Runtime)\r\n// ==========================================\r\nexport interface CompiledWorkflow {\r\n workflow: string;\r\n version: number;\r\n initialState: string; // Optimize: เก็บชื่อ Initial State ไว้เลย ไม่ต้อง loop หา\r\n states: Record<string, CompiledState>;\r\n}\r\n\r\nexport interface CompiledState {\r\n terminal: boolean;\r\n transitions: Record<string, CompiledTransition>;\r\n}\r\n\r\nexport interface CompiledTransition {\r\n to: string;\r\n requirements: {\r\n roles: string[];\r\n userId?: string;\r\n };\r\n condition?: string;\r\n events: RawEvent[];\r\n}\r\n\r\n@Injectable()\r\nexport class WorkflowDslService {\r\n private readonly logger = new Logger(WorkflowDslService.name);\r\n\r\n /**\r\n * [Compile Time]\r\n * แปลง Raw DSL เป็น Compiled Structure พร้อม Validation\r\n */\r\n compile(dsl: RawWorkflowDSL): CompiledWorkflow {\r\n this.validateSchemaStructure(dsl);\r\n\r\n const compiled: CompiledWorkflow = {\r\n workflow: dsl.workflow,\r\n version: dsl.version || 1,\r\n initialState: '',\r\n states: {},\r\n };\r\n\r\n const definedStates = new Set<string>(dsl.states.map((s) => s.name));\r\n let initialFound = false;\r\n\r\n // 1. Process States\r\n for (const rawState of dsl.states) {\r\n if (rawState.initial) {\r\n if (initialFound) {\r\n throw new BadRequestException(\r\n `DSL Error: Multiple initial states found (at \"${rawState.name}\").`\r\n );\r\n }\r\n compiled.initialState = rawState.name;\r\n initialFound = true;\r\n }\r\n\r\n const compiledState: CompiledState = {\r\n terminal: !!rawState.terminal,\r\n transitions: {},\r\n };\r\n\r\n // 2. Process Transitions\r\n if (rawState.on) {\r\n for (const [action, rule] of Object.entries(rawState.on)) {\r\n // Validation: Target state must exist\r\n if (!definedStates.has(rule.to)) {\r\n throw new BadRequestException(\r\n `DSL Error: State \"${rawState.name}\" transitions via \"${action}\" to unknown state \"${rule.to}\".`\r\n );\r\n }\r\n\r\n compiledState.transitions[action] = {\r\n to: rule.to,\r\n requirements: {\r\n roles: rule.require?.role\r\n ? Array.isArray(rule.require.role)\r\n ? rule.require.role\r\n : [rule.require.role]\r\n : [],\r\n userId: rule.require?.user,\r\n },\r\n condition: rule.condition,\r\n events: rule.events || [],\r\n };\r\n }\r\n } else if (!rawState.terminal) {\r\n this.logger.warn(\r\n `State \"${rawState.name}\" is not terminal but has no transitions.`\r\n );\r\n }\r\n\r\n compiled.states[rawState.name] = compiledState;\r\n }\r\n\r\n if (!initialFound) {\r\n throw new BadRequestException('DSL Error: No initial state defined.');\r\n }\r\n\r\n return compiled;\r\n }\r\n\r\n /**\r\n * [Runtime]\r\n * ประมวลผล Action และคืนค่า State ถัดไป\r\n */\r\n evaluate(\r\n compiled: CompiledWorkflow,\r\n currentState: string,\r\n action: string,\r\n context: Record<string, unknown> = {}\r\n ): { nextState: string; events: RawEvent[] } {\r\n const stateConfig = compiled.states[currentState];\r\n\r\n // 1. Validate State Existence\r\n if (!stateConfig) {\r\n throw new BadRequestException(\r\n `Runtime Error: Current state \"${currentState}\" is invalid.`\r\n );\r\n }\r\n\r\n // 2. Check if terminal\r\n if (stateConfig.terminal) {\r\n throw new BadRequestException(\r\n `Runtime Error: Cannot transition from terminal state \"${currentState}\".`\r\n );\r\n }\r\n\r\n // 3. Find Transition\r\n const transition = stateConfig.transitions[action];\r\n if (!transition) {\r\n const allowed = Object.keys(stateConfig.transitions).join(', ');\r\n throw new BadRequestException(\r\n `Invalid Action: \"${action}\" is not allowed from \"${currentState}\". Allowed: [${allowed}]`\r\n );\r\n }\r\n\r\n // 4. Validate Requirements (RBAC)\r\n this.checkRequirements(transition.requirements, context);\r\n\r\n // 5. Evaluate Condition (Dynamic Logic)\r\n if (transition.condition) {\r\n const isMet = this.evaluateCondition(transition.condition, context);\r\n if (!isMet) {\r\n throw new BadRequestException(\r\n 'Condition Failed: The criteria for this transition are not met.'\r\n );\r\n }\r\n }\r\n\r\n return {\r\n nextState: transition.to,\r\n events: transition.events,\r\n };\r\n }\r\n\r\n // --------------------------------------------------------\r\n // Private Helpers\r\n // --------------------------------------------------------\r\n\r\n private validateSchemaStructure(dsl: unknown) {\r\n if (!dsl || typeof dsl !== 'object') {\r\n throw new BadRequestException('DSL must be a JSON object.');\r\n }\r\n const d = dsl as Record<string, unknown>;\r\n if (!d.workflow || !d.states || !Array.isArray(d.states)) {\r\n throw new BadRequestException(\r\n 'DSL Error: Missing required fields (workflow, states).'\r\n );\r\n }\r\n }\r\n\r\n private checkRequirements(\r\n req: CompiledTransition['requirements'],\r\n context: Record<string, unknown>\r\n ) {\r\n // [FIX] Early return if no requirements defined\r\n if (!req) {\r\n return;\r\n }\r\n\r\n const userRoles: string[] = (context.roles as string[]) || [];\r\n const userId: string | number = context.userId as string | number;\r\n\r\n // Check Roles (OR logic inside array) - with null-safety\r\n const requiredRoles = req.roles || [];\r\n if (requiredRoles.length > 0) {\r\n const hasRole = requiredRoles.some((r) => userRoles.includes(r));\r\n if (!hasRole) {\r\n throw new BadRequestException(\r\n `Access Denied: Required roles [${requiredRoles.join(', ')}]`\r\n );\r\n }\r\n }\r\n\r\n // Check Specific User\r\n if (req.userId && String(req.userId) !== String(userId)) {\r\n throw new BadRequestException('Access Denied: User mismatch.');\r\n }\r\n }\r\n\r\n /**\r\n * Evaluate simple JS expression securely\r\n * NOTE: In production, use a safe parser like 'json-logic-js' or vm2\r\n * For this phase, we use a simple Function constructor with restricted scope.\r\n */\r\n private evaluateCondition(\r\n expression: string,\r\n context: Record<string, unknown>\r\n ): boolean {\r\n try {\r\n // Simple guard against malicious code (basic)\r\n if (expression.includes('process') || expression.includes('require')) {\r\n throw new Error('Unsafe expression detected');\r\n }\r\n\r\n // Create a function that returns the expression result\r\n // \"context\" is available inside the expression\r\n const func = new Function('context', `return ${expression};`);\r\n return !!func(context);\r\n } catch (error: unknown) {\r\n this.logger.error(\r\n `Condition Error: \"${expression}\" -> ${error instanceof Error ? error.message : String(error)}`\r\n );\r\n return false; // Fail safe\r\n }\r\n }\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\workflow-engine.controller.ts",
"messages": [
{
"ruleId": "@typescript-eslint/require-await",
"severity": 2,
"message": "Async method 'getAvailableActions' has no 'await' expression.",
"line": 118,
"column": 3,
"nodeType": "FunctionExpression",
"messageId": "missingAwait",
"endLine": 118,
"endColumn": 28,
"suggestions": [
{ "messageId": "removeAsync", "fix": { "range": [4233, 4239], "text": "" }, "desc": "Remove 'async'." }
]
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "// File: src/modules/workflow-engine/workflow-engine.controller.ts\r\n\r\nimport {\r\n Body,\r\n Controller,\r\n Get,\r\n Param,\r\n Patch,\r\n Post,\r\n Request,\r\n UseGuards,\r\n} from '@nestjs/common';\r\nimport {\r\n ApiBearerAuth,\r\n ApiOperation,\r\n ApiParam,\r\n ApiResponse,\r\n ApiTags,\r\n} from '@nestjs/swagger';\r\n\r\n// Services\r\nimport { WorkflowEngineService } from './workflow-engine.service';\r\n\r\n// DTOs\r\nimport { CreateWorkflowDefinitionDto } from './dto/create-workflow-definition.dto';\r\nimport { EvaluateWorkflowDto } from './dto/evaluate-workflow.dto';\r\nimport { UpdateWorkflowDefinitionDto } from './dto/update-workflow-definition.dto';\r\nimport { WorkflowTransitionDto } from './dto/workflow-transition.dto';\r\n\r\n// Guards & Decorators (อ้างอิงตามโครงสร้าง src/common ในแผนงาน)\r\nimport { RequirePermission } from '../../common/decorators/require-permission.decorator';\r\nimport { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';\r\nimport { RbacGuard } from '../../common/guards/rbac.guard';\r\nimport type { RequestWithUser } from '../../common/interfaces/request-with-user.interface';\r\n\r\n@ApiTags('Workflow Engine')\r\n@ApiBearerAuth() // ระบุว่าต้องใช้ Token ใน Swagger\r\n@Controller('workflow-engine')\r\n@UseGuards(JwtAuthGuard, RbacGuard) // บังคับ Login และตรวจสอบสิทธิ์ทุก Request\r\nexport class WorkflowEngineController {\r\n constructor(private readonly workflowService: WorkflowEngineService) {}\r\n\r\n // =================================================================\r\n // Definition Management (Admin / Developer)\r\n // =================================================================\r\n\r\n @Post('definitions')\r\n @ApiOperation({ summary: 'สร้าง Workflow Definition ใหม่ (Auto Versioning)' })\r\n @ApiResponse({ status: 201, description: 'Created successfully' })\r\n // ใช้ Permission 'system.manage_all' (Admin) หรือสร้าง permission ใหม่ 'workflow.manage' ในอนาคต\r\n @RequirePermission('system.manage_all')\r\n async createDefinition(@Body() dto: CreateWorkflowDefinitionDto) {\r\n return this.workflowService.createDefinition(dto);\r\n }\r\n\r\n @Get('definitions')\r\n @ApiOperation({ summary: 'ดึง Workflow Definition ทั้งหมด' })\r\n @RequirePermission('system.manage_all')\r\n async getDefinitions() {\r\n return this.workflowService.getDefinitions();\r\n }\r\n\r\n @Get('definitions/:id')\r\n @ApiOperation({ summary: 'ดึง Workflow Definition ด้วย ID' })\r\n @RequirePermission('system.manage_all')\r\n async getDefinitionById(@Param('id') id: string) {\r\n return this.workflowService.getDefinitionById(id);\r\n }\r\n\r\n @Patch('definitions/:id')\r\n @ApiOperation({ summary: 'แก้ไข Workflow Definition (Re-compile DSL)' })\r\n @RequirePermission('system.manage_all')\r\n async updateDefinition(\r\n @Param('id') id: string,\r\n @Body() dto: UpdateWorkflowDefinitionDto\r\n ) {\r\n return this.workflowService.update(id, dto);\r\n }\r\n\r\n @Post('evaluate')\r\n @ApiOperation({ summary: 'ทดสอบ Logic Workflow (Dry Run) ไม่บันทึกข้อมูล' })\r\n @RequirePermission('system.manage_all')\r\n async evaluate(@Body() dto: EvaluateWorkflowDto) {\r\n return this.workflowService.evaluate(dto);\r\n }\r\n\r\n // =================================================================\r\n // Runtime Engine (User Actions)\r\n // =================================================================\r\n\r\n @Post('instances/:id/transition')\r\n @ApiOperation({ summary: 'สั่งเปลี่ยนสถานะเอกสาร (User Action)' })\r\n @ApiParam({ name: 'id', description: 'Workflow Instance ID (UUID)' })\r\n // Permission จะถูกตรวจสอบ Dynamic ภายใน Service ตาม State ของ Workflow แต่ขั้นต้นต้องมีสิทธิ์ทำงาน Workflow\r\n @RequirePermission('workflow.action_review')\r\n async processTransition(\r\n @Param('id') instanceId: string,\r\n @Body() dto: WorkflowTransitionDto,\r\n @Request() req: RequestWithUser\r\n ) {\r\n // ดึง User ID จาก Token (req.user มาจาก JwtStrategy)\r\n const userId = req.user?.user_id;\r\n\r\n return this.workflowService.processTransition(\r\n instanceId,\r\n dto.action,\r\n userId,\r\n dto.comment,\r\n dto.payload\r\n );\r\n }\r\n\r\n @Get('instances/:id/actions')\r\n @ApiOperation({\r\n summary: 'ดึงรายการปุ่ม Action ที่สามารถกดได้ ณ สถานะปัจจุบัน',\r\n })\r\n @RequirePermission('document.view') // ผู้ที่มีสิทธิ์ดูเอกสาร ควรดู Action ได้\r\n async getAvailableActions(@Param('id') _instanceId: string) {\r\n // Note: Logic การดึง Action ตาม Instance ID จะถูก Implement ใน Task ถัดไป\r\n return { message: 'Pending implementation in Service layer' };\r\n }\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\workflow-engine.module.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\workflow-engine.service.spec.ts",
"messages": [
{
"ruleId": "@typescript-eslint/unbound-method",
"severity": 2,
"message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.",
"line": 121,
"column": 14,
"nodeType": "MemberExpression",
"messageId": "unboundWithoutThisAnnotation",
"endLine": 121,
"endColumn": 32
},
{
"ruleId": "@typescript-eslint/unbound-method",
"severity": 2,
"message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.",
"line": 122,
"column": 14,
"nodeType": "MemberExpression",
"messageId": "unboundWithoutThisAnnotation",
"endLine": 122,
"endColumn": 28
},
{
"ruleId": "@typescript-eslint/unbound-method",
"severity": 2,
"message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.",
"line": 145,
"column": 14,
"nodeType": "MemberExpression",
"messageId": "unboundWithoutThisAnnotation",
"endLine": 145,
"endColumn": 33
},
{
"ruleId": "@typescript-eslint/unbound-method",
"severity": 2,
"message": "A method that is not declared with `this: void` may cause unintentional scoping of `this` when separated from its object.\nConsider using an arrow function or explicitly `.bind()`ing the method to avoid calling the method with an unintended `this` value. \nIf a function does not access `this`, it can be annotated with `this: void`.",
"line": 188,
"column": 14,
"nodeType": "MemberExpression",
"messageId": "unboundWithoutThisAnnotation",
"endLine": 188,
"endColumn": 41
}
],
"suppressedMessages": [],
"errorCount": 4,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import { Test, TestingModule } from '@nestjs/testing';\r\nimport { WorkflowEngineService } from './workflow-engine.service';\r\nimport { getRepositoryToken } from '@nestjs/typeorm';\r\nimport { DataSource, Repository } from 'typeorm';\r\nimport { WorkflowDefinition } from './entities/workflow-definition.entity';\r\nimport {\r\n WorkflowInstance,\r\n WorkflowStatus,\r\n} from './entities/workflow-instance.entity';\r\nimport { WorkflowHistory } from './entities/workflow-history.entity';\r\nimport { WorkflowDslService } from './workflow-dsl.service';\r\nimport { WorkflowEventService } from './workflow-event.service';\r\nimport { NotFoundException } from '@nestjs/common';\r\nimport { CreateWorkflowDefinitionDto } from './dto/create-workflow-definition.dto';\r\n\r\ndescribe('WorkflowEngineService', () => {\r\n let service: WorkflowEngineService;\r\n let defRepo: Repository<WorkflowDefinition>;\r\n let instanceRepo: Repository<WorkflowInstance>;\r\n let dslService: WorkflowDslService;\r\n let eventService: WorkflowEventService;\r\n\r\n // Mock Objects\r\n const mockQueryRunner = {\r\n connect: jest.fn(),\r\n startTransaction: jest.fn(),\r\n commitTransaction: jest.fn(),\r\n rollbackTransaction: jest.fn(),\r\n release: jest.fn(),\r\n manager: {\r\n findOne: jest.fn(),\r\n save: jest.fn(),\r\n },\r\n };\r\n\r\n const mockDataSource = {\r\n createQueryRunner: jest.fn().mockReturnValue(mockQueryRunner),\r\n };\r\n\r\n const mockDslService = {\r\n compile: jest.fn(),\r\n evaluate: jest.fn(),\r\n };\r\n\r\n const mockEventService = {\r\n dispatchEvents: jest.fn(),\r\n };\r\n\r\n const mockCompiledWorkflow = {\r\n initialState: 'START',\r\n states: {\r\n START: { transitions: { SUBMIT: 'PENDING' } },\r\n PENDING: { transitions: { APPROVE: 'APPROVED', REJECT: 'REJECTED' } },\r\n APPROVED: { terminal: true },\r\n REJECTED: { terminal: true },\r\n },\r\n };\r\n\r\n beforeEach(async () => {\r\n const module: TestingModule = await Test.createTestingModule({\r\n providers: [\r\n WorkflowEngineService,\r\n {\r\n provide: getRepositoryToken(WorkflowDefinition),\r\n useValue: {\r\n findOne: jest.fn(),\r\n create: jest.fn(),\r\n save: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: getRepositoryToken(WorkflowInstance),\r\n useValue: {\r\n create: jest.fn(),\r\n save: jest.fn(),\r\n findOne: jest.fn(),\r\n },\r\n },\r\n {\r\n provide: getRepositoryToken(WorkflowHistory),\r\n useValue: {\r\n create: jest.fn(),\r\n save: jest.fn(),\r\n },\r\n },\r\n { provide: WorkflowDslService, useValue: mockDslService },\r\n { provide: WorkflowEventService, useValue: mockEventService },\r\n { provide: DataSource, useValue: mockDataSource },\r\n ],\r\n }).compile();\r\n\r\n service = module.get<WorkflowEngineService>(WorkflowEngineService);\r\n defRepo = module.get(getRepositoryToken(WorkflowDefinition));\r\n instanceRepo = module.get(getRepositoryToken(WorkflowInstance));\r\n dslService = module.get(WorkflowDslService);\r\n eventService = module.get(WorkflowEventService);\r\n\r\n jest.clearAllMocks();\r\n });\r\n\r\n it('should be defined', () => {\r\n expect(service).toBeDefined();\r\n });\r\n\r\n describe('createDefinition', () => {\r\n it('should create a new definition version', async () => {\r\n const dto = {\r\n workflow_code: 'WF01',\r\n dsl: {},\r\n } as CreateWorkflowDefinitionDto;\r\n mockDslService.compile.mockReturnValue(mockCompiledWorkflow);\r\n (defRepo.findOne as jest.Mock).mockResolvedValue({ version: 1 });\r\n (defRepo.create as jest.Mock).mockReturnValue({ version: 2 });\r\n (defRepo.save as jest.Mock).mockResolvedValue({\r\n version: 2,\r\n workflow_code: 'WF01',\r\n });\r\n\r\n const result = await service.createDefinition(dto);\r\n\r\n expect(dslService.compile).toHaveBeenCalledWith(dto.dsl);\r\n expect(defRepo.create).toHaveBeenCalledWith(\r\n expect.objectContaining({ version: 2 })\r\n );\r\n expect(result).toEqual(expect.objectContaining({ version: 2 }));\r\n });\r\n });\r\n\r\n describe('createInstance', () => {\r\n it('should create a new instance with initial state', async () => {\r\n const mockDef = {\r\n id: 'def-1',\r\n compiled: mockCompiledWorkflow,\r\n };\r\n\r\n (defRepo.findOne as jest.Mock).mockResolvedValue(mockDef);\r\n (instanceRepo.create as jest.Mock).mockReturnValue({\r\n id: 'inst-1',\r\n currentState: 'START',\r\n });\r\n (instanceRepo.save as jest.Mock).mockResolvedValue({ id: 'inst-1' });\r\n\r\n const result = await service.createInstance('WF01', 'DOC', '101');\r\n\r\n expect(instanceRepo.create).toHaveBeenCalledWith(\r\n expect.objectContaining({\r\n currentState: 'START',\r\n entityId: '101',\r\n })\r\n );\r\n expect(result).toBeDefined();\r\n });\r\n\r\n it('should throw NotFoundException if definition not found', async () => {\r\n (defRepo.findOne as jest.Mock).mockResolvedValue(null);\r\n await expect(\r\n service.createInstance('WF01', 'DOC', '101')\r\n ).rejects.toThrow(NotFoundException);\r\n });\r\n });\r\n\r\n describe('processTransition', () => {\r\n it('should process transition successfully and commit transaction', async () => {\r\n const instanceId = 'inst-1';\r\n const mockInstance = {\r\n id: instanceId,\r\n currentState: 'PENDING',\r\n status: WorkflowStatus.ACTIVE,\r\n definition: { compiled: mockCompiledWorkflow },\r\n context: { some: 'data' },\r\n };\r\n\r\n // Mock Pessimistic Lock Find\r\n mockQueryRunner.manager.findOne.mockResolvedValue(mockInstance);\r\n\r\n // Mock DSL Evaluation\r\n mockDslService.evaluate.mockReturnValue({\r\n nextState: 'APPROVED',\r\n events: [{ type: 'NOTIFY' }],\r\n });\r\n\r\n const result = await service.processTransition(instanceId, 'APPROVE', 1);\r\n\r\n expect(mockQueryRunner.startTransaction).toHaveBeenCalled();\r\n expect(mockDslService.evaluate).toHaveBeenCalled();\r\n expect(mockQueryRunner.manager.save).toHaveBeenCalledTimes(2); // Instance + History\r\n expect(mockQueryRunner.commitTransaction).toHaveBeenCalled();\r\n expect(eventService.dispatchEvents).toHaveBeenCalled(); // Should dispatch events\r\n expect(result.nextState).toBe('APPROVED');\r\n expect(result.isCompleted).toBe(true);\r\n });\r\n\r\n it('should rollback transaction on error', async () => {\r\n mockQueryRunner.manager.findOne.mockRejectedValue(new Error('DB Error'));\r\n\r\n await expect(\r\n service.processTransition('inst-1', 'APPROVE', 1)\r\n ).rejects.toThrow('DB Error');\r\n\r\n expect(mockQueryRunner.rollbackTransaction).toHaveBeenCalled();\r\n expect(mockQueryRunner.release).toHaveBeenCalled();\r\n });\r\n });\r\n});\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\workflow-engine.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\modules\\workflow-engine\\workflow-event.service.ts",
"messages": [
{
"ruleId": "@typescript-eslint/require-await",
"severity": 2,
"message": "Async method 'processSingleEvent' has no 'await' expression.",
"line": 54,
"column": 3,
"nodeType": "FunctionExpression",
"messageId": "missingAwait",
"endLine": 54,
"endColumn": 35,
"suggestions": [
{ "messageId": "removeAsync", "fix": { "range": [1738, 1744], "text": "" }, "desc": "Remove 'async'." }
]
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "// File: src/modules/workflow-engine/workflow-event.service.ts\r\n\r\nimport { Injectable, Logger } from '@nestjs/common';\r\nimport { RawEvent } from './workflow-dsl.service';\r\n\r\n// Interface สำหรับ External Services ที่จะมารับ Event ต่อ\r\n// (ในอนาคตควรใช้ NestJS Event Emitter เพื่อ Decouple อย่างสมบูรณ์)\r\nexport interface WorkflowEventHandler {\r\n handleNotification(\r\n target: string,\r\n template: string,\r\n payload: Record<string, unknown>\r\n ): Promise<void>;\r\n handleWebhook(url: string, payload: Record<string, unknown>): Promise<void>;\r\n handleAutoAction(instanceId: string, action: string): Promise<void>;\r\n}\r\n\r\n@Injectable()\r\nexport class WorkflowEventService {\r\n private readonly logger = new Logger(WorkflowEventService.name);\r\n\r\n // สามารถ Inject NotificationService หรือ HttpService เข้ามาได้ตรงนี้\r\n // constructor(private readonly notificationService: NotificationService) {}\r\n\r\n /**\r\n * ประมวลผลรายการ Events ที่เกิดจากการเปลี่ยนสถานะ\r\n */\r\n dispatchEvents(\r\n instanceId: string,\r\n events: RawEvent[],\r\n context: Record<string, unknown>\r\n ) {\r\n if (!events || events.length === 0) return;\r\n\r\n this.logger.log(\r\n `Dispatching ${events.length} events for Instance ${instanceId}`\r\n );\r\n\r\n // ทำแบบ Async ไม่รอผล (Fire-and-forget) เพื่อไม่ให้กระทบ Response Time ของ User\r\n void Promise.allSettled(\r\n events.map((event) => this.processSingleEvent(instanceId, event, context))\r\n ).then((results) => {\r\n // Log errors if any\r\n results.forEach((res, idx) => {\r\n if (res.status === 'rejected') {\r\n this.logger.error(\r\n `Failed to process event [${idx}]: ${String(res.reason)}`\r\n );\r\n }\r\n });\r\n });\r\n }\r\n\r\n private async processSingleEvent(\r\n instanceId: string,\r\n event: RawEvent,\r\n context: Record<string, unknown>\r\n ) {\r\n try {\r\n switch (event.type) {\r\n case 'notify':\r\n this.handleNotify(event, context);\r\n break;\r\n case 'webhook':\r\n this.handleWebhook(event, context);\r\n break;\r\n case 'auto_action':\r\n // Logic สำหรับ Auto Transition (เช่น ถ้าผ่านเงื่อนไข ให้ไปต่อเลย)\r\n this.logger.log(`Auto Action triggered for ${instanceId}`);\r\n break;\r\n default:\r\n this.logger.warn(`Unknown event type: ${event.type}`);\r\n }\r\n } catch (error) {\r\n this.logger.error(\r\n `Error processing event ${event.type}: ${String(error)}`\r\n );\r\n throw error;\r\n }\r\n }\r\n\r\n // --- Handlers ---\r\n\r\n private handleNotify(event: RawEvent, _context: Record<string, unknown>) {\r\n // Mockup: ในของจริงจะเรียก NotificationService.send()\r\n // const recipients = this.resolveRecipients(event.target, context);\r\n this.logger.log(\r\n `[EVENT] Notify target: \"${event.target}\" | Template: \"${event.template}\"`\r\n );\r\n }\r\n\r\n private handleWebhook(event: RawEvent, _context: Record<string, unknown>) {\r\n // Mockup: เรียก HttpService.post()\r\n this.logger.log(\r\n `[EVENT] Webhook to: \"${event.target}\" | Payload: ${JSON.stringify(event.payload)}`\r\n );\r\n }\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\redlock.d.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\src\\scripts\\migrate-storage-v2.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-unsafe-argument",
"severity": 2,
"message": "Unsafe argument of type `any` assigned to a parameter of type `DataSourceOptions`.",
"line": 10,
"column": 37,
"nodeType": "TSAsExpression",
"messageId": "unsafeArgument",
"endLine": 10,
"endColumn": 50
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 10,
"column": 47,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 10,
"endColumn": 50,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [446, 449], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [446, 449], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 14,
"column": 5,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 14,
"endColumn": 16,
"suggestions": [
{
"fix": { "range": [502, 553], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "log" },
"desc": "Remove the console.log()."
}
]
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 21,
"column": 5,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 21,
"endColumn": 16,
"suggestions": [
{
"fix": { "range": [766, 832], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "log" },
"desc": "Remove the console.log()."
}
]
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 33,
"column": 5,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 33,
"endColumn": 16,
"suggestions": [
{
"fix": { "range": [1170, 1233], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "log" },
"desc": "Remove the console.log()."
}
]
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 36,
"column": 7,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 36,
"endColumn": 19,
"suggestions": [
{
"fix": { "range": [1288, 1384], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "warn" },
"desc": "Remove the console.warn()."
}
]
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 50,
"column": 9,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 50,
"endColumn": 21,
"suggestions": [
{
"fix": { "range": [1652, 1723], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "warn" },
"desc": "Remove the console.warn()."
}
]
},
{
"ruleId": "no-useless-escape",
"severity": 2,
"message": "Unnecessary escape character: \\/.",
"line": 57,
"column": 20,
"nodeType": "Literal",
"messageId": "unnecessaryEscape",
"endLine": 57,
"endColumn": 21,
"suggestions": [
{
"messageId": "removeEscape",
"fix": { "range": [1913, 1914], "text": "" },
"desc": "Remove the `\\`. This maintains the current functionality."
},
{
"messageId": "escapeBackslash",
"fix": { "range": [1913, 1913], "text": "\\" },
"desc": "Replace the `\\` with `\\\\` to include the actual backslash character."
}
]
},
{
"ruleId": "no-useless-escape",
"severity": 2,
"message": "Unnecessary escape character: \\/.",
"line": 57,
"column": 78,
"nodeType": "Literal",
"messageId": "unnecessaryEscape",
"endLine": 57,
"endColumn": 79,
"suggestions": [
{
"messageId": "removeEscape",
"fix": { "range": [1971, 1972], "text": "" },
"desc": "Remove the `\\`. This maintains the current functionality."
},
{
"messageId": "escapeBackslash",
"fix": { "range": [1971, 1971], "text": "\\" },
"desc": "Replace the `\\` with `\\\\` to include the actual backslash character."
}
]
},
{
"ruleId": "no-useless-escape",
"severity": 2,
"message": "Unnecessary escape character: \\/.",
"line": 57,
"column": 89,
"nodeType": "Literal",
"messageId": "unnecessaryEscape",
"endLine": 57,
"endColumn": 90,
"suggestions": [
{
"messageId": "removeEscape",
"fix": { "range": [1982, 1983], "text": "" },
"desc": "Remove the `\\`. This maintains the current functionality."
},
{
"messageId": "escapeBackslash",
"fix": { "range": [1982, 1982], "text": "\\" },
"desc": "Replace the `\\` with `\\\\` to include the actual backslash character."
}
]
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 68,
"column": 9,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 68,
"endColumn": 21,
"suggestions": [
{
"fix": { "range": [2296, 2353], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "warn" },
"desc": "Remove the console.warn()."
}
]
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 98,
"column": 37,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 98,
"endColumn": 48
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 100,
"column": 9,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 100,
"endColumn": 22,
"suggestions": [
{
"fix": { "range": [3366, 3474], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "error" },
"desc": "Remove the console.error()."
}
]
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 108,
"column": 5,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 108,
"endColumn": 16,
"suggestions": [
{
"fix": { "range": [3521, 3557], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "log" },
"desc": "Remove the console.log()."
}
]
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 109,
"column": 5,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 109,
"endColumn": 16,
"suggestions": [
{
"fix": { "range": [3563, 3599], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "log" },
"desc": "Remove the console.log()."
}
]
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 110,
"column": 5,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 110,
"endColumn": 16,
"suggestions": [
{
"fix": { "range": [3605, 3645], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "log" },
"desc": "Remove the console.log()."
}
]
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 111,
"column": 5,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 111,
"endColumn": 16,
"suggestions": [
{
"fix": { "range": [3651, 3688], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "log" },
"desc": "Remove the console.log()."
}
]
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 113,
"column": 5,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 113,
"endColumn": 18,
"suggestions": [
{
"fix": { "range": [3715, 3757], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "error" },
"desc": "Remove the console.error()."
}
]
},
{
"ruleId": "@typescript-eslint/no-floating-promises",
"severity": 2,
"message": "Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.",
"line": 119,
"column": 1,
"nodeType": "ExpressionStatement",
"messageId": "floatingVoid",
"endLine": 119,
"endColumn": 18,
"suggestions": [
{
"messageId": "floatingFixVoid",
"fix": { "range": [3817, 3817], "text": "void " },
"desc": "Add void operator to ignore."
},
{
"messageId": "floatingFixAwait",
"fix": { "range": [3817, 3817], "text": "await " },
"desc": "Add await operator."
}
]
}
],
"suppressedMessages": [],
"errorCount": 19,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import { DataSource } from 'typeorm';\r\nimport { databaseConfig } from '../config/database.config';\r\nimport { Attachment } from '../common/file-storage/entities/attachment.entity';\r\nimport * as fs from 'fs-extra';\r\nimport * as path from 'path';\r\n\r\nasync function migrateStorage() {\r\n // Config override for script execution if needed\r\n const config = { ...databaseConfig, entities: [Attachment] };\r\n const dataSource = new DataSource(config as any);\r\n await dataSource.initialize();\r\n\r\n try {\r\n console.log('🚀 Starting Storage Migration v2...');\r\n const attachmentRepo = dataSource.getRepository(Attachment);\r\n\r\n // Find all permanent attachments\r\n const attachments = await attachmentRepo.find({\r\n where: { isTemporary: false },\r\n });\r\n console.log(`Found ${attachments.length} permanent attachments.`);\r\n\r\n let movedCount = 0;\r\n let errorCount = 0;\r\n let skippedCount = 0;\r\n\r\n // Define base permanent directory\r\n // Note: Adjust path based on execution context (e.g., from backend root)\r\n const permanentBaseDir =\r\n process.env.UPLOAD_PERMANENT_DIR ||\r\n path.join(process.cwd(), 'uploads', 'permanent');\r\n\r\n console.log(`Target Permanent Directory: ${permanentBaseDir}`);\r\n\r\n if (!fs.existsSync(permanentBaseDir)) {\r\n console.warn(\r\n `Base directory not found: ${permanentBaseDir}. Creating it...`\r\n );\r\n await fs.ensureDir(permanentBaseDir);\r\n }\r\n\r\n for (const att of attachments) {\r\n if (!att.filePath) {\r\n skippedCount++;\r\n continue;\r\n }\r\n\r\n const currentPath = att.filePath;\r\n if (!fs.existsSync(currentPath)) {\r\n console.warn(`File not found on disk: ${currentPath} (ID: ${att.id})`);\r\n errorCount++;\r\n continue;\r\n }\r\n\r\n // Check if already in new structure (contains /General/YYYY/MM or similar)\r\n const newStructureRegex =\r\n /permanent[\\/\\\\](ContractDrawing|ShopDrawing|AsBuiltDrawing|General)[\\/\\\\]\\d{4}[\\/\\\\]\\d{2}/;\r\n if (newStructureRegex.test(currentPath)) {\r\n skippedCount++;\r\n continue;\r\n }\r\n\r\n // Determine target date\r\n const refDate = att.referenceDate\r\n ? new Date(att.referenceDate)\r\n : new Date(att.createdAt);\r\n if (isNaN(refDate.getTime())) {\r\n console.warn(`Invalid date for ID ${att.id}, skipping.`);\r\n errorCount++;\r\n continue;\r\n }\r\n\r\n const year = refDate.getFullYear().toString();\r\n const month = (refDate.getMonth() + 1).toString().padStart(2, '0');\r\n\r\n // Determine Doc Type (Default 'General' as we don't know easily without joins)\r\n const docType = 'General';\r\n\r\n const newDir = path.join(permanentBaseDir, docType, year, month);\r\n const newPath = path.join(newDir, att.storedFilename);\r\n\r\n if (path.resolve(currentPath) === path.resolve(newPath)) {\r\n skippedCount++;\r\n continue;\r\n }\r\n\r\n try {\r\n await fs.ensureDir(newDir);\r\n await fs.move(currentPath, newPath, { overwrite: true });\r\n\r\n // Update DB\r\n att.filePath = newPath;\r\n if (!att.referenceDate) {\r\n att.referenceDate = refDate;\r\n }\r\n await attachmentRepo.save(att);\r\n movedCount++;\r\n if (movedCount % 100 === 0) console.log(`Moved ${movedCount} files...`);\r\n } catch (err: unknown) {\r\n console.error(\r\n `Failed to move file ID ${att.id}:`,\r\n (err as Error).message\r\n );\r\n errorCount++;\r\n }\r\n }\r\n\r\n console.log(`Migration completed.`);\r\n console.log(`Moved: ${movedCount}`);\r\n console.log(`Skipped: ${skippedCount}`);\r\n console.log(`Errors: ${errorCount}`);\r\n } catch (error) {\r\n console.error('Migration failed:', error);\r\n } finally {\r\n await dataSource.destroy();\r\n }\r\n}\r\n\r\nmigrateStorage();\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\test\\app.e2e-spec.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\test\\phase3-workflow.e2e-spec.ts",
"messages": [
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 57,
"column": 7,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 57,
"endColumn": 19,
"suggestions": [
{
"fix": { "range": [1934, 2037], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "warn" },
"desc": "Remove the console.warn()."
}
]
},
{
"ruleId": "@typescript-eslint/no-unsafe-argument",
"severity": 2,
"message": "Unsafe argument of type `any` assigned to a parameter of type `App`.",
"line": 70,
"column": 36,
"nodeType": "CallExpression",
"messageId": "unsafeArgument",
"endLine": 70,
"endColumn": 55
},
{
"ruleId": "@typescript-eslint/no-unsafe-assignment",
"severity": 2,
"message": "Unsafe assignment of an `any` value.",
"line": 83,
"column": 5,
"nodeType": "AssignmentExpression",
"messageId": "anyAssignment",
"endLine": 83,
"endColumn": 40
},
{
"ruleId": "@typescript-eslint/no-unsafe-member-access",
"severity": 2,
"message": "Unsafe member access .id on an `any` value.",
"line": 83,
"column": 38,
"nodeType": "Identifier",
"messageId": "unsafeMemberExpression",
"endLine": 83,
"endColumn": 40
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 84,
"column": 5,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 84,
"endColumn": 16,
"suggestions": [
{
"fix": { "range": [2707, 2767], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "log" },
"desc": "Remove the console.log()."
}
]
},
{
"ruleId": "@typescript-eslint/no-unsafe-argument",
"severity": 2,
"message": "Unsafe argument of type `any` assigned to a parameter of type `App`.",
"line": 88,
"column": 36,
"nodeType": "CallExpression",
"messageId": "unsafeArgument",
"endLine": 88,
"endColumn": 55
},
{
"ruleId": "@typescript-eslint/no-unsafe-assignment",
"severity": 2,
"message": "Unsafe assignment of an `any` value.",
"line": 98,
"column": 5,
"nodeType": "AssignmentExpression",
"messageId": "anyAssignment",
"endLine": 98,
"endColumn": 50
},
{
"ruleId": "@typescript-eslint/no-unsafe-member-access",
"severity": 2,
"message": "Unsafe member access .instanceId on an `any` value.",
"line": 98,
"column": 40,
"nodeType": "Identifier",
"messageId": "unsafeMemberExpression",
"endLine": 98,
"endColumn": 50
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 99,
"column": 5,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 99,
"endColumn": 16,
"suggestions": [
{
"fix": { "range": [3290, 3347], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "log" },
"desc": "Remove the console.log()."
}
]
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 100,
"column": 5,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 100,
"endColumn": 16,
"suggestions": [
{
"fix": { "range": [3353, 3411], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "log" },
"desc": "Remove the console.log()."
}
]
},
{
"ruleId": "@typescript-eslint/no-unsafe-member-access",
"severity": 2,
"message": "Unsafe member access .currentState on an `any` value.",
"line": 100,
"column": 49,
"nodeType": "Identifier",
"messageId": "unsafeMemberExpression",
"endLine": 100,
"endColumn": 61
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 106,
"column": 7,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 106,
"endColumn": 19,
"suggestions": [
{
"fix": { "range": [3592, 3657], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "warn" },
"desc": "Remove the console.warn()."
}
]
},
{
"ruleId": "@typescript-eslint/no-unsafe-argument",
"severity": 2,
"message": "Unsafe argument of type `any` assigned to a parameter of type `App`.",
"line": 110,
"column": 36,
"nodeType": "CallExpression",
"messageId": "unsafeArgument",
"endLine": 110,
"endColumn": 55
},
{
"ruleId": "no-console",
"severity": 2,
"message": "Unexpected console statement.",
"line": 122,
"column": 5,
"nodeType": "MemberExpression",
"messageId": "unexpected",
"endLine": 122,
"endColumn": 16,
"suggestions": [
{
"fix": { "range": [4216, 4261], "text": "" },
"messageId": "removeConsole",
"data": { "propertyName": "log" },
"desc": "Remove the console.log()."
}
]
}
],
"suppressedMessages": [],
"errorCount": 14,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import { Test, TestingModule } from '@nestjs/testing';\r\nimport { INestApplication } from '@nestjs/common';\r\nimport request from 'supertest';\r\nimport { AppModule } from '../src/app.module';\r\nimport { JwtService } from '@nestjs/jwt';\r\nimport { DataSource } from 'typeorm';\r\nimport { WorkflowDefinition } from '../src/modules/workflow-engine/entities/workflow-definition.entity';\r\n\r\n/**\r\n * Phase 3 Workflow (E2E) - Unified Workflow Engine\r\n *\r\n * Tests the correspondence workflow using the Unified Workflow Engine\r\n * instead of the deprecated RoutingTemplate system.\r\n */\r\ndescribe('Phase 3 Workflow (E2E)', () => {\r\n let app: INestApplication;\r\n let jwtService: JwtService;\r\n let dataSource: DataSource;\r\n let correspondenceId: number;\r\n let workflowInstanceId: string;\r\n\r\n // Test Users (must exist in seed data)\r\n const editorUser = { user_id: 3, username: 'editor01', organization_id: 41 };\r\n const adminUser = { user_id: 2, username: 'admin', organization_id: 1 };\r\n\r\n let editorToken: string;\r\n let _adminToken: string;\r\n\r\n beforeAll(async () => {\r\n const moduleFixture: TestingModule = await Test.createTestingModule({\r\n imports: [AppModule],\r\n }).compile();\r\n\r\n app = moduleFixture.createNestApplication();\r\n await app.init();\r\n\r\n jwtService = moduleFixture.get<JwtService>(JwtService);\r\n dataSource = moduleFixture.get<DataSource>(DataSource);\r\n\r\n // Generate Tokens\r\n editorToken = jwtService.sign({\r\n username: editorUser.username,\r\n sub: editorUser.user_id,\r\n });\r\n adminToken = jwtService.sign({\r\n username: adminUser.username,\r\n sub: adminUser.user_id,\r\n });\r\n\r\n // Ensure workflow definition exists (should be seeded)\r\n const defRepo = dataSource.getRepository(WorkflowDefinition);\r\n const existing = await defRepo.findOne({\r\n where: { workflow_code: 'CORRESPONDENCE_FLOW_V1', is_active: true },\r\n });\r\n\r\n if (!existing) {\r\n console.warn(\r\n 'WorkflowDefinition CORRESPONDENCE_FLOW_V1 not found. Tests may fail.'\r\n );\r\n }\r\n });\r\n\r\n afterAll(async () => {\r\n if (app) {\r\n await app.close();\r\n }\r\n });\r\n\r\n it('/correspondences (POST) - Create Document', async () => {\r\n const response = await request(app.getHttpServer())\r\n .post('/correspondences')\r\n .set('Authorization', `Bearer ${editorToken}`)\r\n .send({\r\n projectId: 1,\r\n typeId: 1,\r\n title: 'E2E Workflow Test Document',\r\n details: { question: 'Testing Unified Workflow' },\r\n })\r\n .expect(201);\r\n\r\n expect(response.body).toHaveProperty('id');\r\n expect(response.body).toHaveProperty('correspondenceNumber');\r\n correspondenceId = response.body.id;\r\n console.log('Created Correspondence ID:', correspondenceId);\r\n });\r\n\r\n it('/correspondences/:id/submit (POST) - Submit to Workflow', async () => {\r\n const response = await request(app.getHttpServer())\r\n .post(`/correspondences/${correspondenceId}/submit`)\r\n .set('Authorization', `Bearer ${editorToken}`)\r\n .send({\r\n note: 'Submitting for E2E test',\r\n })\r\n .expect(201);\r\n\r\n expect(response.body).toHaveProperty('instanceId');\r\n expect(response.body).toHaveProperty('currentState');\r\n workflowInstanceId = response.body.instanceId;\r\n console.log('Workflow Instance ID:', workflowInstanceId);\r\n console.log('Current State:', response.body.currentState);\r\n });\r\n\r\n it('/correspondences/:id/workflow/action (POST) - Process Action', async () => {\r\n // Skip if submit failed to get instanceId\r\n if (!workflowInstanceId) {\r\n console.warn('Skipping action test - no instanceId from submit');\r\n return;\r\n }\r\n\r\n const response = await request(app.getHttpServer())\r\n .post(`/correspondences/${correspondenceId}/workflow/action`)\r\n .set('Authorization', `Bearer ${editorToken}`) // Use editor - has workflow.action_review permission\r\n .send({\r\n instanceId: workflowInstanceId,\r\n action: 'APPROVE',\r\n comment: 'E2E Approved via Unified Workflow Engine',\r\n })\r\n .expect(201);\r\n\r\n expect(response.body).toHaveProperty('success', true);\r\n expect(response.body).toHaveProperty('nextState');\r\n console.log('Action Result:', response.body);\r\n });\r\n});\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\backend\\test\\simple.e2e-spec.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'request' is defined but never used. Allowed unused vars must match /^_/u.",
"line": 1,
"column": 8,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 1,
"endColumn": 15,
"suggestions": [
{
"messageId": "removeUnusedImportDeclaration",
"data": { "varName": "request" },
"fix": { "range": [0, 34], "text": "" },
"desc": "Remove unused import declaration."
}
]
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import request from 'supertest';\r\nimport { AppModule } from '../src/app.module';\r\nimport { _RoutingTemplate } from '../src/modules/correspondence/entities/routing-template.entity';\r\n\r\nimport { Test, TestingModule } from '@nestjs/testing';\r\n\r\ndescribe('Simple Test', () => {\r\n it('should pass', async () => {\r\n const moduleFixture: TestingModule = await Test.createTestingModule({\r\n imports: [AppModule],\r\n }).compile();\r\n expect(moduleFixture).toBeDefined();\r\n });\r\n});\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\eslint.config.mjs",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\fix-console.cjs",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\fix-lint-automated.cjs",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\fix-parseInt.cjs",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\access-control\\organizations\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\access-control\\roles\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\access-control\\users\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\audit-logs\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\contracts\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\drawings\\contract\\categories\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\drawings\\contract\\sub-categories\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\drawings\\contract\\volumes\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\drawings\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\drawings\\shop\\main-categories\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\drawings\\shop\\sub-categories\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\numbering\\[id]\\edit\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\numbering\\new\\page.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'_error' is defined but never used.",
"line": 28,
"column": 14,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 28,
"endColumn": 20
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "\"use client\";\r\n\r\nimport { TemplateEditor } from \"@/components/numbering/template-editor\";\r\nimport { numberingApi, NumberingTemplate } from \"@/lib/api/numbering\";\r\nimport { useRouter } from \"next/navigation\";\r\nimport { useCorrespondenceTypes, useContracts, useDisciplines } from \"@/hooks/use-master-data\";\r\nimport { useProjects } from \"@/hooks/use-projects\";\r\nimport { toast } from 'sonner';\r\n\r\nexport default function NewTemplatePage() {\r\n const router = useRouter();\r\n\r\n // Master Data\r\n const { data: correspondenceTypes = [] } = useCorrespondenceTypes();\r\n const { data: projects = [] } = useProjects();\r\n const projectId = 1; // Default or sync with selection\r\n const { data: contracts = [] } = useContracts(projectId);\r\n const contractId = contracts[0]?.id;\r\n const { data: disciplines = [] } = useDisciplines(contractId);\r\n\r\n const selectedProjectName =\r\n projects.find((p: { id: number; projectName: string }) => p.id === projectId)?.projectName || 'LCBP3';\r\n\r\n const handleSave = async (data: Partial<NumberingTemplate>) => {\r\n try {\r\n await numberingApi.saveTemplate(data);\r\n router.push(\"/admin/numbering\");\r\n } catch (_error) {\r\n toast.error('Failed to create template');\r\n }\r\n };\r\n\r\n const handleCancel = () => {\r\n router.push('/admin/doc-control/numbering');\r\n };\r\n\r\n return (\r\n <div className=\"p-6 space-y-6\">\r\n <h1 className=\"text-3xl font-bold\">New Numbering Template</h1>\r\n <TemplateEditor\r\n projectId={projectId}\r\n projectName={selectedProjectName}\r\n correspondenceTypes={correspondenceTypes}\r\n disciplines={disciplines}\r\n onSave={handleSave}\r\n onCancel={handleCancel}\r\n />\r\n </div>\r\n );\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\numbering\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\projects\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\reference\\correspondence-types\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\reference\\disciplines\\page.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 17,
"column": 28,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 17,
"endColumn": 31,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [742, 745], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [742, 745], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 46,
"column": 45,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 46,
"endColumn": 48,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [1497, 1500], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [1497, 1500], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'id' is defined but never used. Allowed unused args must match /^_/u.",
"line": 70,
"column": 20,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 70,
"endColumn": 22
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 84,
"column": 36,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 84,
"endColumn": 39,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [3233, 3236], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [3233, 3236], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
}
],
"suppressedMessages": [],
"errorCount": 4,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "'use client';\n\nimport { GenericCrudTable } from '@/components/admin/reference/generic-crud-table';\nimport { masterDataService } from '@/lib/services/master-data.service';\nimport { useContracts } from '@/hooks/use-master-data';\nimport { ColumnDef } from '@tanstack/react-table';\nimport { useState } from 'react';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\n\nexport default function DisciplinesPage() {\n const [selectedContractId, setSelectedContractId] = useState<string | null>(null);\n\n const { data: contractsData = [] } = useContracts();\n // Ensure we consistently use an array\n const contracts = Array.isArray(contractsData) ? contractsData : [];\n\n const columns: ColumnDef<any>[] = [\n {\n accessorKey: 'disciplineCode',\n header: 'Code',\n cell: ({ row }) => <span className=\"font-mono font-bold\">{row.getValue('disciplineCode')}</span>,\n },\n {\n accessorKey: 'codeNameTh',\n header: 'Name (TH)',\n },\n {\n accessorKey: 'codeNameEn',\n header: 'Name (EN)',\n },\n {\n accessorKey: 'isActive',\n header: 'Status',\n cell: ({ row }) => (\n <span\n className={`px-2 py-1 rounded-full text-xs ${\n row.getValue('isActive') ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'\n }`}\n >\n {row.getValue('isActive') ? 'Active' : 'Inactive'}\n </span>\n ),\n },\n ];\n\n const contractOptions = contracts.map((c: any) => ({\n label: `${c.contractName} (${c.contractCode})`,\n value: String(c.id),\n }));\n\n return (\n <div className=\"p-6\">\n <GenericCrudTable\n entityName=\"Discipline\"\n title=\"Disciplines Management\"\n description=\"Manage system disciplines (e.g., ARCH, STR, MEC)\"\n queryKey={['disciplines', selectedContractId ?? 'all']}\n fetchFn={async () => {\n const items = await masterDataService.getDisciplines(selectedContractId ? selectedContractId : undefined);\n // ADR-019: Map contractId INT → contract UUID for edit mode select matching\n return (items as Record<string, unknown>[]).map((item) => {\n const rec = item as { contract?: { id?: number; uuid?: string }; contractId?: number };\n return {\n ...item,\n contractId: rec.contract?.id || rec.contract?.uuid || String(rec.contractId),\n };\n });\n }}\n createFn={(data) => masterDataService.createDiscipline(data as unknown as Parameters<typeof masterDataService.createDiscipline>[0])}\n updateFn={(id, _data) => Promise.reject('Not implemented yet')}\n deleteFn={(id) => masterDataService.deleteDiscipline(id)}\n columns={columns}\n filters={\n <div className=\"w-[300px]\">\n <Select\n value={selectedContractId || 'all'}\n onValueChange={(val) => setSelectedContractId(val === 'all' ? null : val)}\n >\n <SelectTrigger>\n <SelectValue placeholder=\"Filter by Contract\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All Contracts</SelectItem>\n {contracts.map((c: any) => (\n <SelectItem key={c.id} value={String(c.id)}>\n {c.contractName} ({c.contractCode})\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n }\n fields={[\n {\n name: 'contractId',\n label: 'Contract',\n type: 'select',\n required: true,\n options: contractOptions,\n },\n {\n name: 'disciplineCode',\n label: 'Code',\n type: 'text',\n required: true,\n },\n {\n name: 'codeNameTh',\n label: 'Name (TH)',\n type: 'text',\n required: true,\n },\n { name: 'codeNameEn', label: 'Name (EN)', type: 'text' },\n { name: 'isActive', label: 'Active', type: 'checkbox' },\n ]}\n />\n </div>\n );\n}\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\reference\\drawing-categories\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\reference\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\reference\\rfa-types\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\reference\\tags\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\workflows\\[id]\\edit\\page.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'_error' is defined but never used.",
"line": 72,
"column": 14,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 72,
"endColumn": 20
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "'use client';\r\n\r\nimport { useState, useEffect } from 'react';\r\nimport { useRouter, useParams } from 'next/navigation';\r\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';\r\nimport { DSLEditor } from '@/components/workflows/dsl-editor';\r\nimport { VisualWorkflowBuilder } from '@/components/workflows/visual-builder';\r\nimport { Button } from '@/components/ui/button';\r\nimport { Input } from '@/components/ui/input';\r\nimport { Label } from '@/components/ui/label';\r\nimport { Textarea } from '@/components/ui/textarea';\r\nimport { Card } from '@/components/ui/card';\r\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\r\nimport { useWorkflowDefinition, useCreateWorkflowDefinition, useUpdateWorkflowDefinition } from '@/hooks/use-workflows';\r\nimport { Workflow } from '@/types/workflow';\r\nimport { CreateWorkflowDefinitionDto } from '@/types/dto/workflow-engine/workflow-engine.dto';\r\nimport { toast } from 'sonner';\r\nimport { Save, ArrowLeft, Loader2 } from 'lucide-react';\r\nimport Link from 'next/link';\r\n\r\nexport default function WorkflowEditPage() {\r\n const params = useParams();\r\n const router = useRouter();\r\n const id = params?.id === 'new' ? null : params?.id as string;\r\n\r\n const [workflowData, setWorkflowData] = useState<Partial<Workflow>>({\r\n workflowName: '',\r\n description: '',\r\n workflowType: 'CORRESPONDENCE',\r\n dslDefinition: 'name: New Workflow\\nversion: 1.0\\nsteps: []',\r\n isActive: true,\r\n });\r\n\r\n const { data: fetchedWorkflow, isLoading: loadingWorkflow } = useWorkflowDefinition(id as string);\r\n const createMutation = useCreateWorkflowDefinition();\r\n const updateMutation = useUpdateWorkflowDefinition();\r\n\r\n useEffect(() => {\r\n if (fetchedWorkflow) {\r\n setWorkflowData(fetchedWorkflow);\r\n }\r\n }, [fetchedWorkflow]);\r\n\r\n const loading = (!!id && loadingWorkflow) || createMutation.isPending || updateMutation.isPending;\r\n const saving = createMutation.isPending || updateMutation.isPending;\r\n\r\n const handleSave = async () => {\r\n if (!workflowData.workflowName) {\r\n toast.error('Workflow name is required');\r\n return;\r\n }\r\n\r\n try {\r\n const dto: CreateWorkflowDefinitionDto = {\r\n workflow_code: workflowData.workflowType || 'CORRESPONDENCE',\r\n dsl: {\r\n workflowName: workflowData.workflowName,\r\n description: workflowData.description,\r\n dslDefinition: workflowData.dslDefinition,\r\n },\r\n is_active: workflowData.isActive,\r\n };\r\n\r\n if (id) {\r\n await updateMutation.mutateAsync({ id, data: dto });\r\n toast.success('Workflow updated successfully');\r\n } else {\r\n await createMutation.mutateAsync(dto);\r\n toast.success('Workflow created successfully');\r\n router.push('/admin/doc-control/workflows');\r\n }\r\n } catch (_error) {\r\n toast.error('Failed to save workflow');\r\n }\r\n };\r\n\r\n if (loading) {\r\n return (\r\n <div className=\"flex items-center justify-center h-screen\">\r\n <Loader2 className=\"h-8 w-8 animate-spin\" />\r\n </div>\r\n );\r\n }\r\n\r\n return (\r\n <div className=\"p-6 space-y-6 max-w-7xl mx-auto\">\r\n <div className=\"flex justify-between items-center\">\r\n <div className=\"flex items-center gap-4\">\r\n <Link href=\"/admin/doc-control/workflows\">\r\n <Button variant=\"ghost\" size=\"icon\">\r\n <ArrowLeft className=\"h-5 w-5\" />\r\n </Button>\r\n </Link>\r\n <div>\r\n <h1 className=\"text-3xl font-bold\">{id ? 'Edit Workflow' : 'New Workflow'}</h1>\r\n <p className=\"text-muted-foreground\">\r\n {id ? `Version ${workflowData.version}` : 'Create a new workflow definition'}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"flex gap-2\">\r\n <Link href=\"/admin/doc-control/workflows\">\r\n <Button variant=\"outline\">Cancel</Button>\r\n </Link>\r\n <Button onClick={handleSave} disabled={saving}>\r\n {saving && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\r\n <Save className=\"mr-2 h-4 w-4\" />\r\n {id ? 'Save Changes' : 'Create Workflow'}\r\n </Button>\r\n </div>\r\n </div>\r\n\r\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\r\n <div className=\"lg:col-span-1 space-y-6\">\r\n <Card className=\"p-6\">\r\n <div className=\"grid gap-4\">\r\n <div>\r\n <Label htmlFor=\"name\">Workflow Name *</Label>\r\n <Input\r\n id=\"name\"\r\n value={workflowData.workflowName}\r\n onChange={(e) =>\r\n setWorkflowData({\r\n ...workflowData,\r\n workflowName: e.target.value,\r\n })\r\n }\r\n placeholder=\"e.g. Standard RFA Workflow\"\r\n />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"desc\">Description</Label>\r\n <Textarea\r\n id=\"desc\"\r\n value={workflowData.description}\r\n onChange={(e) =>\r\n setWorkflowData({\r\n ...workflowData,\r\n description: e.target.value,\r\n })\r\n }\r\n placeholder=\"Describe the purpose of this workflow\"\r\n />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"type\">Workflow Type</Label>\r\n <Select\r\n value={workflowData.workflowType}\r\n onValueChange={(value: Workflow['workflowType']) =>\r\n setWorkflowData({ ...workflowData, workflowType: value })\r\n }\r\n >\r\n <SelectTrigger>\r\n <SelectValue placeholder=\"Select type\" />\r\n </SelectTrigger>\r\n <SelectContent>\r\n <SelectItem value=\"CORRESPONDENCE\">Correspondence</SelectItem>\r\n <SelectItem value=\"RFA\">RFA</SelectItem>\r\n <SelectItem value=\"DRAWING\">Drawing</SelectItem>\r\n </SelectContent>\r\n </Select>\r\n </div>\r\n </div>\r\n </Card>\r\n </div>\r\n\r\n <div className=\"lg:col-span-2\">\r\n <Tabs defaultValue=\"dsl\" className=\"w-full\">\r\n <TabsList className=\"w-full justify-start\">\r\n <TabsTrigger value=\"dsl\">DSL Editor</TabsTrigger>\r\n <TabsTrigger value=\"visual\">Visual Builder</TabsTrigger>\r\n </TabsList>\r\n\r\n <TabsContent value=\"dsl\" className=\"mt-4\">\r\n <DSLEditor\r\n initialValue={workflowData.dslDefinition}\r\n onChange={(value) => setWorkflowData({ ...workflowData, dslDefinition: value })}\r\n />\r\n </TabsContent>\r\n\r\n <TabsContent value=\"visual\" className=\"mt-4 h-[600px]\">\r\n <VisualWorkflowBuilder\r\n dslString={workflowData.dslDefinition}\r\n onDslChange={(newDsl) => setWorkflowData({ ...workflowData, dslDefinition: newDsl })}\r\n onSave={() => toast.info('Visual state saving not implemented in this demo')}\r\n />\r\n </TabsContent>\r\n </Tabs>\r\n </div>\r\n </div>\r\n </div>\r\n );\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\workflows\\new\\page.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'_error' is defined but never used.",
"line": 34,
"column": 14,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 34,
"endColumn": 20
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "'use client';\r\n\r\nimport { useState } from 'react';\r\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';\r\nimport { DSLEditor } from '@/components/workflows/dsl-editor';\r\nimport { VisualWorkflowBuilder } from '@/components/workflows/visual-builder';\r\nimport { Button } from '@/components/ui/button';\r\nimport { Input } from '@/components/ui/input';\r\nimport { Label } from '@/components/ui/label';\r\nimport { Textarea } from '@/components/ui/textarea';\r\nimport { Card } from '@/components/ui/card';\r\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\r\nimport { workflowApi } from '@/lib/api/workflows';\r\nimport { WorkflowType } from '@/types/workflow';\r\nimport { useRouter } from 'next/navigation';\r\nimport { Loader2 } from 'lucide-react';\r\nimport { toast } from 'sonner';\r\n\r\nexport default function NewWorkflowPage() {\r\n const router = useRouter();\r\n const [saving, setSaving] = useState(false);\r\n const [workflowData, setWorkflowData] = useState({\r\n workflowName: '',\r\n description: '',\r\n workflowType: 'CORRESPONDENCE' as WorkflowType,\r\n dslDefinition: 'name: New Workflow\\nversion: 1.0\\nsteps: []',\r\n });\r\n\r\n const handleSave = async () => {\r\n setSaving(true);\r\n try {\r\n await workflowApi.createWorkflow(workflowData);\r\n router.push('/admin/doc-control/workflows');\r\n } catch (_error) {\r\n toast.error('Failed to create workflow');\r\n } finally {\r\n setSaving(false);\r\n }\r\n };\r\n\r\n return (\r\n <div className=\"p-6 space-y-6\">\r\n <div className=\"flex justify-between items-center\">\r\n <h1 className=\"text-3xl font-bold\">New Workflow</h1>\r\n <div className=\"flex gap-2\">\r\n <Button variant=\"outline\" onClick={() => router.back()}>\r\n Cancel\r\n </Button>\r\n <Button onClick={handleSave} disabled={saving}>\r\n {saving && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\r\n Create Workflow\r\n </Button>\r\n </div>\r\n </div>\r\n\r\n <Card className=\"p-6\">\r\n <div className=\"grid gap-4\">\r\n <div>\r\n <Label htmlFor=\"workflow_name\">Workflow Name *</Label>\r\n <Input\r\n id=\"workflow_name\"\r\n value={workflowData.workflowName}\r\n onChange={(e) =>\r\n setWorkflowData({\r\n ...workflowData,\r\n workflowName: e.target.value,\r\n })\r\n }\r\n placeholder=\"e.g., Special RFA Approval\"\r\n />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"description\">Description</Label>\r\n <Textarea\r\n id=\"description\"\r\n value={workflowData.description}\r\n onChange={(e) =>\r\n setWorkflowData({\r\n ...workflowData,\r\n description: e.target.value,\r\n })\r\n }\r\n placeholder=\"Describe the purpose of this workflow\"\r\n />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"workflow_type\">Workflow Type</Label>\r\n <Select\r\n value={workflowData.workflowType}\r\n onValueChange={(value) => setWorkflowData({ ...workflowData, workflowType: value as WorkflowType })}\r\n >\r\n <SelectTrigger id=\"workflow_type\">\r\n <SelectValue />\r\n </SelectTrigger>\r\n <SelectContent>\r\n <SelectItem value=\"CORRESPONDENCE\">Correspondence</SelectItem>\r\n <SelectItem value=\"RFA\">RFA</SelectItem>\r\n <SelectItem value=\"DRAWING\">Drawing</SelectItem>\r\n </SelectContent>\r\n </Select>\r\n </div>\r\n </div>\r\n </Card>\r\n\r\n <Tabs defaultValue=\"dsl\">\r\n <TabsList>\r\n <TabsTrigger value=\"dsl\">DSL Editor</TabsTrigger>\r\n <TabsTrigger value=\"visual\">Visual Builder</TabsTrigger>\r\n </TabsList>\r\n\r\n <TabsContent value=\"dsl\" className=\"mt-4\">\r\n <DSLEditor\r\n initialValue={workflowData.dslDefinition}\r\n onChange={(value) => setWorkflowData({ ...workflowData, dslDefinition: value })}\r\n />\r\n </TabsContent>\r\n\r\n <TabsContent value=\"visual\" className=\"mt-4\">\r\n <VisualWorkflowBuilder />\r\n </TabsContent>\r\n </Tabs>\r\n </div>\r\n );\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\doc-control\\workflows\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\migration\\errors\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\migration\\page.tsx",
"messages": [
{
"ruleId": "react-hooks/exhaustive-deps",
"severity": 1,
"message": "React Hook useEffect has a missing dependency: 'fetchData'. Either include it or remove the dependency array.",
"line": 35,
"column": 6,
"nodeType": "ArrayExpression",
"endLine": 35,
"endColumn": 20,
"suggestions": [
{
"desc": "Update the dependencies array to be: [fetchData, statusFilter]",
"fix": { "range": [1363, 1377], "text": "[fetchData, statusFilter]" }
}
]
},
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'error' is defined but never used.",
"line": 104,
"column": 14,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 104,
"endColumn": 19
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 1,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { migrationService } from \"@/lib/services/migration.service\";\nimport { MigrationReviewQueueItem, MigrationReviewStatus } from \"@/types/migration\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/components/ui/table\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { toast } from \"sonner\";\nimport { Button } from \"@/components/ui/button\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { format } from \"date-fns\";\nimport { EyeIcon, FileXIcon, CheckSquareIcon } from \"lucide-react\";\nimport Link from \"next/link\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { getApiErrorMessage } from \"@/types/api-error\";\n\nexport default function MigrationReviewQueuePage() {\n const [items, setItems] = useState<MigrationReviewQueueItem[]>([]);\n const [loading, setLoading] = useState(true);\n const [submitting, setSubmitting] = useState(false);\n const [statusFilter, setStatusFilter] = useState<string>(\"PENDING\");\n const [selectedIds, setSelectedIds] = useState<number[]>([]);\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n\n useEffect(() => {\n fetchData();\n }, [statusFilter]);\n\n const fetchData = async () => {\n try {\n setLoading(true);\n setErrorMessage(null);\n const res = await migrationService.getReviewQueue({\n status: statusFilter === \"ALL\" ? undefined : (statusFilter as MigrationReviewStatus),\n limit: 50,\n });\n setItems(Array.isArray(res.items) ? res.items : []);\n setSelectedIds([]); // reset selection on fetch\n } catch (error: unknown) {\n setItems([]);\n setErrorMessage(getApiErrorMessage(error, \"Failed to load queue\"));\n } finally {\n setLoading(false);\n }\n };\n\n const handleToggleSelectAll = () => {\n if (selectedIds.length === items.length) {\n setSelectedIds([]);\n } else {\n setSelectedIds(items.map((i) => i.id));\n }\n };\n\n const handleToggleSelect = (id: number) => {\n setSelectedIds((prev) =>\n prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]\n );\n };\n\n const handleBatchApprove = async () => {\n if (selectedIds.length === 0) return;\n try {\n setSubmitting(true);\n\n const batchItems = items\n .filter((i) => selectedIds.includes(i.id))\n .map((item) => ({\n queueId: item.id,\n dto: {\n document_number: item.documentNumber,\n subject: item.title || item.originalTitle || 'Untitled',\n category: item.aiSuggestedCategory || 'Correspondence',\n project_id: item.projectId || 1,\n migrated_by: 'SYSTEM_IMPORT',\n temp_attachment_id: item.tempAttachmentId,\n ai_confidence: item.aiConfidence,\n ai_issues: item.aiIssues,\n issued_date: item.issuedDate,\n received_date: item.receivedDate,\n sender_id: item.senderOrganizationId,\n receiver_id: item.receiverOrganizationId,\n details: {\n tags: item.extractedTags\n }\n }\n }));\n\n const batchId = `BATCH_UI_${Date.now()}`;\n await migrationService.commitBatch(\n { items: batchItems, batchId },\n batchId\n );\n\n fetchData();\n } catch (error) {\n toast.error(\"Batch commit failed.\");\n } finally {\n setSubmitting(false);\n }\n };\n\n return (\n <div className=\"space-y-6\">\n <div className=\"flex justify-between flex-wrap gap-4 items-center\">\n <div>\n <h1 className=\"text-3xl font-bold tracking-tight\">Migration Review Queue</h1>\n <p className=\"text-muted-foreground mt-1\">\n Review and correct documents that AI flagged as low confidence.\n </p>\n </div>\n <div className=\"flex items-center gap-4\">\n {selectedIds.length > 0 && (\n <Button\n variant=\"default\"\n onClick={handleBatchApprove}\n disabled={submitting}\n >\n <CheckSquareIcon className=\"mr-2 h-4 w-4\" />\n {submitting ? \"Processing...\" : `Batch Approve (${selectedIds.length})`}\n </Button>\n )}\n <Link href=\"/admin/migration/errors\">\n <Button variant=\"outline\">\n <FileXIcon className=\"mr-2 h-4 w-4\" /> View Error Logs\n </Button>\n </Link>\n <Select value={statusFilter} onValueChange={setStatusFilter}>\n <SelectTrigger className=\"w-[180px]\">\n <SelectValue placeholder=\"Status\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All Status</SelectItem>\n <SelectItem value=\"PENDING\">Pending</SelectItem>\n <SelectItem value=\"APPROVED\">Approved</SelectItem>\n <SelectItem value=\"REJECTED\">Rejected</SelectItem>\n </SelectContent>\n </Select>\n </div>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>Queue Items - {statusFilter}</CardTitle>\n </CardHeader>\n <CardContent>\n {errorMessage && (\n <div className=\"mb-4 rounded-md border border-destructive/30 bg-destructive/5 px-4 py-3 text-sm text-destructive\">\n {errorMessage}\n </div>\n )}\n {loading ? (\n <div className=\"py-10 text-center\">Loading queue...</div>\n ) : items.length === 0 ? (\n <div className=\"py-10 text-center text-muted-foreground\">No items in the queue.</div>\n ) : (\n <div className=\"rounded-md border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead className=\"w-[50px]\">\n <Checkbox\n checked={items.length > 0 && selectedIds.length === items.length}\n onCheckedChange={handleToggleSelectAll}\n aria-label=\"Select all\"\n />\n </TableHead>\n <TableHead>Document No.</TableHead>\n <TableHead>Suggested Category</TableHead>\n <TableHead>Confidence</TableHead>\n <TableHead>Status</TableHead>\n <TableHead>Created At</TableHead>\n <TableHead className=\"text-right\">Action</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {items.map((item) => (\n <TableRow key={item.id}>\n <TableCell>\n <Checkbox\n checked={selectedIds.includes(item.id)}\n onCheckedChange={() => handleToggleSelect(item.id)}\n aria-label={`Select item ${item.id}`}\n />\n </TableCell>\n <TableCell className=\"font-medium\">{item.documentNumber}</TableCell>\n <TableCell>{item.aiSuggestedCategory || \"Unknown\"}</TableCell>\n <TableCell>\n <Badge\n variant={\n !item.aiConfidence\n ? \"destructive\"\n : item.aiConfidence > 0.8\n ? \"default\"\n : item.aiConfidence > 0.5\n ? \"secondary\"\n : \"destructive\"\n }\n >\n {item.aiConfidence ? (item.aiConfidence * 100).toFixed(1) + \"%\" : \"N/A\"}\n </Badge>\n </TableCell>\n <TableCell>\n <Badge variant={item.status === 'PENDING' ? 'outline' : item.status === 'APPROVED' ? 'default' : 'destructive'}>\n {item.status}\n </Badge>\n </TableCell>\n <TableCell>{format(new Date(item.createdAt), \"dd MMM yyyy, HH:mm\")}</TableCell>\n <TableCell className=\"text-right\">\n <Link href={`/admin/migration/review/${item.id}`}>\n <Button size=\"sm\" variant=\"ghost\">\n <EyeIcon className=\"h-4 w-4 mr-2\" /> Review\n </Button>\n </Link>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n )}\n </CardContent>\n </Card>\n </div>\n );\n}\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\migration\\review\\[id]\\page.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'FormDescription' is defined but never used. Allowed unused vars must match /^_/u.",
"line": 14,
"column": 3,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 14,
"endColumn": 18,
"suggestions": [
{
"messageId": "removeUnusedVar",
"data": { "varName": "FormDescription" },
"fix": { "range": [450, 469], "text": "" },
"desc": "Remove unused variable \"FormDescription\"."
}
]
},
{
"ruleId": "react-hooks/exhaustive-deps",
"severity": 1,
"message": "React Hook useEffect has a missing dependency: 'fetchItem'. Either include it or remove the dependency array.",
"line": 67,
"column": 6,
"nodeType": "ArrayExpression",
"endLine": 67,
"endColumn": 10,
"suggestions": [
{
"desc": "Update the dependencies array to be: [fetchItem, id]",
"fix": { "range": [2120, 2124], "text": "[fetchItem, id]" }
}
]
},
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'error' is defined but never used.",
"line": 89,
"column": 14,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 89,
"endColumn": 19
},
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'error' is defined but never used.",
"line": 144,
"column": 14,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 144,
"endColumn": 19
}
],
"suppressedMessages": [],
"errorCount": 3,
"fatalErrorCount": 0,
"warningCount": 1,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { useParams, useRouter } from \"next/navigation\";\nimport { useForm } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport * as z from \"zod\";\nimport { migrationService } from \"@/lib/services/migration.service\";\nimport { MigrationReviewQueueItem } from \"@/types/migration\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Form,\n FormControl,\n FormDescription,\n FormField,\n FormItem,\n FormLabel,\n FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\nimport { ArrowLeftIcon, CheckCircleIcon, XCircleIcon } from \"lucide-react\";\nimport Link from \"next/link\";\nimport { toast } from \"sonner\";\nimport { Card, CardContent } from \"@/components/ui/card\";\n\nconst reviewFormSchema = z.object({\n document_number: z.string().min(1, \"Document number is required\"),\n subject: z.string().min(1, \"Subject is required\"),\n category: z.string().min(1, \"Category is required\"),\n document_date: z.string().optional(),\n issued_date: z.string().optional(),\n received_date: z.string().optional(),\n sender_id: z.string().optional(),\n discipline_id: z.string().optional(),\n});\n\ntype ReviewFormValues = z.infer<typeof reviewFormSchema>;\n\nexport default function MigrationReviewPage() {\n const params = useParams();\n const router = useRouter();\n const id = Number(params.id);\n\n const [item, setItem] = useState<MigrationReviewQueueItem | null>(null);\n const [loading, setLoading] = useState(true);\n const [submitting, setSubmitting] = useState(false);\n\n const form = useForm<ReviewFormValues>({\n resolver: zodResolver(reviewFormSchema),\n defaultValues: {\n document_number: \"\",\n subject: \"\",\n category: \"\",\n document_date: \"\",\n issued_date: \"\",\n received_date: \"\",\n sender_id: \"\",\n discipline_id: \"\",\n },\n });\n\n useEffect(() => {\n if (!id) return;\n fetchItem(id);\n }, [id]);\n\n const fetchItem = async (itemId: number) => {\n try {\n setLoading(true);\n const res = await migrationService.getQueueItem(itemId);\n setItem(res);\n\n if (res) {\n // Pre-fill form from database item and aiIssues payload\n const issues = res.aiIssues || {};\n form.reset({\n document_number: res.documentNumber || \"\",\n subject: res.title || res.originalTitle || \"\",\n category: res.aiSuggestedCategory || \"\",\n document_date: issues.document_date || \"\",\n issued_date: issues.issued_date || \"\",\n received_date: issues.received_date || \"\",\n sender_id: issues.sender_id ? String(issues.sender_id) : \"\",\n discipline_id: issues.discipline_id ? String(issues.discipline_id) : \"\",\n });\n }\n } catch (error) {\n toast.error(\"Failed to load queue item\");\n } finally {\n setLoading(false);\n }\n };\n\n const onSubmit = async (values: ReviewFormValues) => {\n if (!item) return;\n\n try {\n setSubmitting(true);\n const issues = item.aiIssues || {};\n\n const payload = {\n document_number: values.document_number,\n subject: values.subject,\n category: values.category,\n source_file_path: issues.source_file_path || \"\",\n migrated_by: \"SYSTEM_IMPORT\",\n batch_id: \"MANUAL_REVIEW_BATCH\",\n project_id: 1, // Assumption or pulled from store\n document_date: values.document_date,\n issued_date: values.issued_date,\n received_date: values.received_date,\n sender_id: values.sender_id ? Number(values.sender_id) : undefined,\n discipline_id: values.discipline_id ? Number(values.discipline_id) : undefined,\n details: {\n tags: issues.tags || [],\n ai_confidence: item.aiConfidence,\n }\n };\n\n // Mock idempotency key based on timestamp to ensure uniqueness per approval retry\n const idempotencyKey = `review-${item.id}-${Date.now()}`;\n await migrationService.approveQueueItem(item.id, payload, idempotencyKey);\n\n toast.success(\"Document approved and imported successfully\");\n router.push(\"/admin/migration\");\n } catch (error: unknown) {\n const err = error as { response?: { data?: { message?: string } } };\n toast.error(err?.response?.data?.message || \"Failed to approve and import\");\n } finally {\n setSubmitting(false);\n }\n };\n\n const onReject = async () => {\n if (!item || !confirm(\"Are you sure you want to REJECT this document? It will not be imported.\")) return;\n\n try {\n setSubmitting(true);\n await migrationService.rejectQueueItem(item.id);\n toast.success(\"Document rejected\");\n router.push(\"/admin/migration\");\n } catch (error: unknown) {\n toast.error(\"Failed to reject document\");\n } finally {\n setSubmitting(false);\n }\n };\n\n if (loading) {\n return <div className=\"py-10 text-center\">Loading document data...</div>;\n }\n\n if (!item) {\n return <div className=\"py-10 text-center text-red-500\">Document not found</div>;\n }\n\n const pdfUrl = item.aiIssues?.source_file_path\n ? migrationService.getStagingFileUrl(item.aiIssues.source_file_path)\n : null;\n\n return (\n <div className=\"flex flex-col h-[calc(100vh-6rem)] space-y-4\">\n <div className=\"flex justify-between items-center shrink-0\">\n <div className=\"flex items-center gap-4\">\n <Link href=\"/admin/migration\">\n <Button variant=\"outline\" size=\"icon\">\n <ArrowLeftIcon className=\"h-4 w-4\" />\n </Button>\n </Link>\n <div>\n <h1 className=\"text-2xl font-bold tracking-tight\">Review Document: {item.documentNumber}</h1>\n <p className=\"text-sm text-muted-foreground flex items-center gap-2\">\n Status: <span className=\"font-semibold text-primary\">{item.status}</span>\n {' | '} Confidence: <span className={item.aiConfidence && item.aiConfidence < 0.8 ? \"text-red-500\" : \"text-green-500\"}>{item.aiConfidence ? (item.aiConfidence * 100).toFixed(1) + \"%\" : \"N/A\"}</span>\n </p>\n </div>\n </div>\n </div>\n\n <div className=\"flex flex-1 gap-6 overflow-hidden\">\n {/* Left Side: PDF Viewer */}\n <Card className=\"flex-1 hidden md:flex flex-col overflow-hidden border-2 border-primary/10 shadow-md\">\n <CardContent className=\"p-0 flex-1 relative bg-slate-100\">\n {pdfUrl ? (\n <iframe\n src={`${pdfUrl}#toolbar=0&navpanes=0`}\n className=\"absolute inset-0 w-full h-full\"\n title=\"Document Viewer\"\n />\n ) : (\n <div className=\"absolute inset-0 flex items-center justify-center text-muted-foreground\">\n <p>No Source File Path found for this document</p>\n </div>\n )}\n </CardContent>\n </Card>\n\n {/* Right Side: Form */}\n <Card className=\"w-full md:w-[450px] lg:w-[500px] flex-shrink-0 flex flex-col overflow-hidden border-2 border-primary/10 shadow-md\">\n <div className=\"p-4 border-b bg-muted/30\">\n <h2 className=\"font-semibold text-lg flex items-center gap-2\">\n Extracted Information\n </h2>\n {item.reviewReason && (\n <p className=\"text-sm text-red-500 mt-1 font-medium bg-red-50 p-2 rounded border border-red-100\">\n Reason: {item.reviewReason}\n </p>\n )}\n </div>\n <CardContent className=\"flex-1 overflow-y-auto p-4 custom-scrollbar\">\n <Form {...form}>\n <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-4\">\n <FormField\n control={form.control}\n name=\"document_number\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Document Number</FormLabel>\n <FormControl>\n <Input {...field} />\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name=\"subject\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Subject</FormLabel>\n <FormControl>\n <Textarea {...field} rows={3} />\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n\n <div className=\"grid grid-cols-2 gap-4\">\n <FormField\n control={form.control}\n name=\"category\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Category</FormLabel>\n <Select onValueChange={field.onChange} defaultValue={field.value}>\n <FormControl>\n <SelectTrigger>\n <SelectValue placeholder=\"Select type\" />\n </SelectTrigger>\n </FormControl>\n <SelectContent>\n <SelectItem value=\"CORR\">CORR</SelectItem>\n <SelectItem value=\"RFA\">RFA</SelectItem>\n <SelectItem value=\"LETTER\">LETTER</SelectItem>\n <SelectItem value=\"MEMO\">MEMO</SelectItem>\n </SelectContent>\n </Select>\n <FormMessage />\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name=\"discipline_id\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Discipline ID</FormLabel>\n <FormControl>\n <Input {...field} type=\"number\" placeholder=\"Optional\" />\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <FormField\n control={form.control}\n name=\"document_date\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Doc Date</FormLabel>\n <FormControl>\n <Input {...field} type=\"date\" />\n </FormControl>\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name=\"issued_date\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Issued Date</FormLabel>\n <FormControl>\n <Input {...field} type=\"date\" />\n </FormControl>\n </FormItem>\n )}\n />\n </div>\n\n <FormField\n control={form.control}\n name=\"sender_id\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Sender Org ID</FormLabel>\n <FormControl>\n <Input {...field} type=\"number\" placeholder=\"Optional\" />\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n\n {item.aiIssues?.key_points && item.aiIssues.key_points.length > 0 && (\n <div className=\"mt-6 border-t pt-4\">\n <h3 className=\"font-semibold text-sm mb-2 text-muted-foreground\">AI Extracted Key Points</h3>\n <ul className=\"text-sm space-y-1 list-disc pl-4 text-muted-foreground\">\n {item.aiIssues.key_points.map((point: string, i: number) => (\n <li key={i}>{point}</li>\n ))}\n </ul>\n </div>\n )}\n\n <div className=\"flex gap-4 pt-6 mt-4 border-t sticky bottom-0 bg-background/95 backdrop-blur z-10\">\n <Button\n type=\"button\"\n variant=\"destructive\"\n className=\"flex-1\"\n disabled={submitting || item.status !== 'PENDING'}\n onClick={onReject}\n >\n <XCircleIcon className=\"w-4 h-4 mr-2\" />\n Reject\n </Button>\n <Button\n type=\"submit\"\n className=\"flex-1 bg-green-600 hover:bg-green-700 text-white\"\n disabled={submitting || item.status !== 'PENDING'}\n >\n <CheckCircleIcon className=\"w-4 h-4 mr-2\" />\n {submitting ? \"Processing...\" : \"Approve & Import\"}\n </Button>\n </div>\n </form>\n </Form>\n </CardContent>\n </Card>\n </div>\n </div>\n );\n}\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\monitoring\\audit-logs\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\monitoring\\sessions\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\monitoring\\system-logs\\numbering\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\numbering\\[id]\\edit\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\numbering\\new\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\numbering\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\organizations\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\settings\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\users\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\workflows\\[id]\\edit\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\workflows\\new\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\admin\\workflows\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\error.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(admin)\\layout.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(auth)\\layout.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(auth)\\login\\page.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'_error' is defined but never used.",
"line": 78,
"column": 14,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 78,
"endColumn": 20
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "// File: app/(auth)/login/page.tsx\r\n\"use client\";\r\n\r\nimport { useState } from \"react\";\r\nimport { useForm } from \"react-hook-form\";\r\nimport { zodResolver } from \"@hookform/resolvers/zod\";\r\nimport * as z from \"zod\";\r\nimport { signIn } from \"next-auth/react\";\r\nimport { useRouter } from \"next/navigation\";\r\nimport { Eye, EyeOff, Loader2 } from \"lucide-react\";\r\nimport { toast } from \"sonner\";\r\n\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Card,\r\n CardContent,\r\n CardDescription,\r\n CardFooter,\r\n CardHeader,\r\n CardTitle,\r\n} from \"@/components/ui/card\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\n\r\n// กำหนด Schema สำหรับตรวจสอบข้อมูลฟอร์ม\r\nconst loginSchema = z.object({\r\n username: z.string().min(1, \"กรุณาระบุชื่อผู้ใช้งาน\"),\r\n password: z.string().min(1, \"กรุณาระบุรหัสผ่าน\"),\r\n});\r\n\r\ntype LoginValues = z.infer<typeof loginSchema>;\r\n\r\nexport default function LoginPage() {\r\n const router = useRouter();\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [showPassword, setShowPassword] = useState(false);\r\n // Removed local errorMessage state in favor of toast\r\n\r\n // ตั้งค่า React Hook Form\r\n const {\r\n register,\r\n handleSubmit,\r\n formState: { errors },\r\n } = useForm<LoginValues>({\r\n resolver: zodResolver(loginSchema),\r\n defaultValues: {\r\n username: \"\",\r\n password: \"\",\r\n },\r\n });\r\n\r\n // ฟังก์ชันเมื่อกด Submit\r\n async function onSubmit(data: LoginValues) {\r\n setIsLoading(true);\r\n\r\n try {\r\n // เรียกใช้ NextAuth signIn (Credential Provider)\r\n const result = await signIn(\"credentials\", {\r\n username: data.username,\r\n password: data.password,\r\n redirect: false, // เราจะจัดการ Redirect เอง\r\n });\r\n\r\n if (result?.error) {\r\n // กรณี Login ไม่สำเร็จ\r\n toast.error(\"เข้าสู่ระบบไม่สำเร็จ\", {\r\n description: \"ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง กรุณาลองใหม่\",\r\n });\r\n return;\r\n }\r\n\r\n // Login สำเร็จ -> ไปหน้า Dashboard\r\n toast.success(\"เข้าสู่ระบบสำเร็จ\", {\r\n description: \"กำลังพาท่านไปยังหน้า Dashboard...\",\r\n });\r\n router.push(\"/dashboard\");\r\n router.refresh(); // Refresh เพื่อให้ Server Component รับรู้ Session ใหม่\r\n } catch (_error) {\r\n toast.error(\"เกิดข้อผิดพลาด\", {\r\n description: \"ระบบขัดข้อง กรุณาลองใหม่อีกครั้ง หรือติดต่อผู้ดูแลระบบ\",\r\n });\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n }\r\n\r\n return (\r\n <Card className=\"w-full max-w-sm shadow-lg\">\r\n <CardHeader className=\"space-y-1 text-center\">\r\n <CardTitle className=\"text-2xl font-bold text-primary\">\r\n LCBP3 DMS\r\n </CardTitle>\r\n <CardDescription>\r\n กรอกชื่อผู้ใช้งานและรหัสผ่านเพื่อเข้าสู่ระบบ\r\n </CardDescription>\r\n </CardHeader>\r\n\r\n <form onSubmit={handleSubmit(onSubmit)}>\r\n <CardContent className=\"grid gap-4\">\r\n {/* Username Field */}\r\n <div className=\"grid gap-2\">\r\n <Label htmlFor=\"username\">ชื่อผู้ใช้งาน</Label>\r\n <Input\r\n id=\"username\"\r\n placeholder=\"username\"\r\n type=\"text\"\r\n autoCapitalize=\"none\"\r\n autoComplete=\"username\"\r\n autoCorrect=\"off\"\r\n disabled={isLoading}\r\n className={errors.username ? \"border-destructive\" : \"\"}\r\n {...register(\"username\")}\r\n />\r\n {errors.username && (\r\n <p className=\"text-xs text-destructive\">\r\n {errors.username.message}\r\n </p>\r\n )}\r\n </div>\r\n\r\n {/* Password Field */}\r\n <div className=\"grid gap-2\">\r\n <Label htmlFor=\"password\">รหัสผ่าน</Label>\r\n <div className=\"relative\">\r\n <Input\r\n id=\"password\"\r\n placeholder=\"••••••••\"\r\n type={showPassword ? \"text\" : \"password\"}\r\n autoComplete=\"current-password\"\r\n disabled={isLoading}\r\n className={errors.password ? \"border-destructive pr-10\" : \"pr-10\"}\r\n {...register(\"password\")}\r\n />\r\n {/* ปุ่ม Show/Hide Password */}\r\n <Button\r\n type=\"button\"\r\n variant=\"ghost\"\r\n size=\"sm\"\r\n className=\"absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent\"\r\n onClick={() => setShowPassword(!showPassword)}\r\n disabled={isLoading}\r\n >\r\n {showPassword ? (\r\n <EyeOff className=\"h-4 w-4 text-muted-foreground\" />\r\n ) : (\r\n <Eye className=\"h-4 w-4 text-muted-foreground\" />\r\n )}\r\n <span className=\"sr-only\">\r\n {showPassword ? \"Hide password\" : \"Show password\"}\r\n </span>\r\n </Button>\r\n </div>\r\n {errors.password && (\r\n <p className=\"text-xs text-destructive\">\r\n {errors.password.message}\r\n </p>\r\n )}\r\n </div>\r\n </CardContent>\r\n\r\n <CardFooter>\r\n <Button className=\"w-full\" type=\"submit\" disabled={isLoading}>\r\n {isLoading && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\r\n เข้าสู่ระบบ\r\n </Button>\r\n </CardFooter>\r\n </form>\r\n </Card>\r\n );\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\circulation\\[uuid]\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\circulation\\new\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\circulation\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\correspondences\\[uuid]\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\correspondences\\new\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\correspondences\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\dashboard\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\drawings\\[uuid]\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\drawings\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\drawings\\upload\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\error.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\layout.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\profile\\page.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'_error' is defined but never used.",
"line": 70,
"column": 14,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 70,
"endColumn": 20
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "// File: app/(dashboard)/profile/page.tsx\r\n\"use client\";\r\n\r\nimport { useState } from \"react\";\r\nimport { useSession } from \"next-auth/react\";\r\nimport { useForm } from \"react-hook-form\";\r\nimport { zodResolver } from \"@hookform/resolvers/zod\";\r\nimport * as z from \"zod\";\r\nimport { Loader2, User, Shield, Bell } from \"lucide-react\";\r\n\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\r\nimport {\r\n Card,\r\n CardContent,\r\n CardDescription,\r\n CardFooter,\r\n CardHeader,\r\n CardTitle,\r\n} from \"@/components/ui/card\";\r\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\r\nimport { Switch } from \"@/components/ui/switch\";\r\nimport apiClient from \"@/lib/api/client\";\r\nimport { toast } from \"sonner\";\r\n\r\n// -----------------------------------------------------------------------------\r\n// Schemas\r\n// -----------------------------------------------------------------------------\r\n\r\nconst passwordSchema = z\r\n .object({\r\n currentPassword: z.string().min(1, \"กรุณาระบุรหัสผ่านปัจจุบัน\"),\r\n newPassword: z.string().min(8, \"รหัสผ่านใหม่ต้องมีอย่างน้อย 8 ตัวอักษร\"),\r\n confirmPassword: z.string().min(1, \"กรุณายืนยันรหัสผ่านใหม่\"),\r\n })\r\n .refine((data) => data.newPassword === data.confirmPassword, {\r\n message: \"รหัสผ่านใหม่ไม่ตรงกัน\",\r\n path: [\"confirmPassword\"],\r\n });\r\n\r\ntype PasswordValues = z.infer<typeof passwordSchema>;\r\n\r\nexport default function ProfilePage() {\r\n const { data: session } = useSession();\r\n const [isLoading, setIsLoading] = useState(false);\r\n\r\n // --- Password Form Handling ---\r\n const {\r\n register,\r\n handleSubmit,\r\n reset,\r\n formState: { errors },\r\n } = useForm<PasswordValues>({\r\n resolver: zodResolver(passwordSchema),\r\n });\r\n\r\n const onPasswordSubmit = async (data: PasswordValues) => {\r\n setIsLoading(true);\r\n try {\r\n // เรียก API เปลี่ยนรหัสผ่าน\r\n await apiClient.put(\"/users/change-password\", {\r\n currentPassword: data.currentPassword,\r\n newPassword: data.newPassword,\r\n });\r\n\r\n toast.success('เปลี่ยนรหัสผ่านสำเร็จ');\r\n reset();\r\n } catch (_error) {\r\n toast.error('ไม่สามารถเปลี่ยนรหัสผ่านได้: รหัสผ่านปัจจุบันไม่ถูกต้อง');\r\n // Password change failed - toast shown\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n };\r\n\r\n // --- Notification State (Mockup) ---\r\n // ในการใช้งานจริง ควรดึงค่าจาก API /users/preferences หรือ UserPreferenceService\r\n const [notifyEmail, setNotifyEmail] = useState(true);\r\n const [notifyLine, setNotifyLine] = useState(true);\r\n const [digestMode, setDigestMode] = useState(false);\r\n\r\n // Helper to get initials\r\n const userName = session?.user?.name || \"User\";\r\n const userInitials = userName\r\n .split(\" \")\r\n .map((n) => n[0])\r\n .join(\"\")\r\n .toUpperCase()\r\n .substring(0, 2);\r\n\r\n return (\r\n <div className=\"space-y-6\">\r\n <div>\r\n <h3 className=\"text-lg font-medium\">Profile & Settings</h3>\r\n <p className=\"text-sm text-muted-foreground\">\r\n จัดการข้อมูลส่วนตัวและการตั้งค่าระบบของคุณ\r\n </p>\r\n </div>\r\n\r\n <Tabs defaultValue=\"general\" className=\"space-y-4\">\r\n <TabsList>\r\n <TabsTrigger value=\"general\" className=\"flex items-center gap-2\">\r\n <User className=\"h-4 w-4\" />\r\n General\r\n </TabsTrigger>\r\n <TabsTrigger value=\"security\" className=\"flex items-center gap-2\">\r\n <Shield className=\"h-4 w-4\" />\r\n Security\r\n </TabsTrigger>\r\n <TabsTrigger value=\"notifications\" className=\"flex items-center gap-2\">\r\n <Bell className=\"h-4 w-4\" />\r\n Notifications\r\n </TabsTrigger>\r\n </TabsList>\r\n\r\n {/* 1. General Tab */}\r\n <TabsContent value=\"general\">\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>ข้อมูลทั่วไป</CardTitle>\r\n <CardDescription>\r\n ข้อมูลพื้นฐานของคุณที่แสดงในระบบ\r\n </CardDescription>\r\n </CardHeader>\r\n <CardContent className=\"space-y-6\">\r\n <div className=\"flex items-center gap-4\">\r\n <Avatar className=\"h-20 w-20\">\r\n <AvatarImage src={session?.user?.image || \"\"} />\r\n <AvatarFallback className=\"text-lg\">{userInitials}</AvatarFallback>\r\n </Avatar>\r\n <div>\r\n <h4 className=\"text-lg font-semibold\">{userName}</h4>\r\n <p className=\"text-sm text-muted-foreground\">{session?.user?.email}</p>\r\n <div className=\"mt-2 inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-primary text-primary-foreground hover:bg-primary/80\">\r\n {session?.user?.role || \"Member\"}\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div className=\"grid gap-4 md:grid-cols-2\">\r\n <div className=\"space-y-2\">\r\n <Label>ชื่อจริง</Label>\r\n <Input defaultValue={userName.split(\" \")[0]} disabled />\r\n </div>\r\n <div className=\"space-y-2\">\r\n <Label>นามสกุล</Label>\r\n <Input defaultValue={userName.split(\" \")[1] || \"\"} disabled />\r\n </div>\r\n <div className=\"space-y-2\">\r\n <Label>อีเมล</Label>\r\n <Input defaultValue={session?.user?.email || \"\"} disabled />\r\n </div>\r\n <div className=\"space-y-2\">\r\n <Label>หน่วยงาน / องค์กร</Label>\r\n {/* ในอนาคตดึงจาก Organization ID */}\r\n <Input defaultValue={`Organization ID: ${session?.user?.organizationId || \"-\"}`} disabled />\r\n </div>\r\n </div>\r\n </CardContent>\r\n {/* <CardFooter>\r\n <Button>บันทึกการเปลี่ยนแปลง</Button>\r\n </CardFooter>\r\n */}\r\n </Card>\r\n </TabsContent>\r\n\r\n {/* 2. Security Tab */}\r\n <TabsContent value=\"security\">\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>รหัสผ่าน</CardTitle>\r\n <CardDescription>\r\n เปลี่ยนรหัสผ่านเพื่อความปลอดภัยของบัญชี (ต้องมีอย่างน้อย 8 ตัวอักษร)\r\n </CardDescription>\r\n </CardHeader>\r\n <form onSubmit={handleSubmit(onPasswordSubmit)}>\r\n <CardContent className=\"space-y-4\">\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"currentPassword\">รหัสผ่านปัจจุบัน</Label>\r\n <Input\r\n id=\"currentPassword\"\r\n type=\"password\"\r\n {...register(\"currentPassword\")}\r\n />\r\n {errors.currentPassword && (\r\n <p className=\"text-xs text-destructive\">{errors.currentPassword.message}</p>\r\n )}\r\n </div>\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"newPassword\">รหัสผ่านใหม่</Label>\r\n <Input\r\n id=\"newPassword\"\r\n type=\"password\"\r\n {...register(\"newPassword\")}\r\n />\r\n {errors.newPassword && (\r\n <p className=\"text-xs text-destructive\">{errors.newPassword.message}</p>\r\n )}\r\n </div>\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"confirmPassword\">ยืนยันรหัสผ่านใหม่</Label>\r\n <Input\r\n id=\"confirmPassword\"\r\n type=\"password\"\r\n {...register(\"confirmPassword\")}\r\n />\r\n {errors.confirmPassword && (\r\n <p className=\"text-xs text-destructive\">{errors.confirmPassword.message}</p>\r\n )}\r\n </div>\r\n </CardContent>\r\n <CardFooter>\r\n <Button type=\"submit\" disabled={isLoading}>\r\n {isLoading && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\r\n เปลี่ยนรหัสผ่าน\r\n </Button>\r\n </CardFooter>\r\n </form>\r\n </Card>\r\n </TabsContent>\r\n\r\n {/* 3. Notifications Tab */}\r\n <TabsContent value=\"notifications\">\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>การแจ้งเตือน</CardTitle>\r\n <CardDescription>\r\n กำหนดช่องทางที่คุณต้องการรับข้อมูลข่าวสารจากระบบ\r\n </CardDescription>\r\n </CardHeader>\r\n <CardContent className=\"space-y-6\">\r\n <div className=\"flex items-center justify-between space-x-2\">\r\n <Label htmlFor=\"notify-email\" className=\"flex flex-col space-y-1\">\r\n <span>Email Notifications</span>\r\n <span className=\"font-normal text-xs text-muted-foreground\">\r\n รับแจ้งเตือนงานใหม่และการอนุมัติผ่านทางอีเมล\r\n </span>\r\n </Label>\r\n <Switch\r\n id=\"notify-email\"\r\n checked={notifyEmail}\r\n onCheckedChange={setNotifyEmail}\r\n />\r\n </div>\r\n\r\n <div className=\"flex items-center justify-between space-x-2\">\r\n <Label htmlFor=\"notify-line\" className=\"flex flex-col space-y-1\">\r\n <span>LINE Notifications</span>\r\n <span className=\"font-normal text-xs text-muted-foreground\">\r\n รับแจ้งเตือนด่วนผ่าน LINE Official Account\r\n </span>\r\n </Label>\r\n <Switch\r\n id=\"notify-line\"\r\n checked={notifyLine}\r\n onCheckedChange={setNotifyLine}\r\n />\r\n </div>\r\n\r\n <div className=\"flex items-center justify-between space-x-2\">\r\n <Label htmlFor=\"digest-mode\" className=\"flex flex-col space-y-1\">\r\n <span>Digest Mode (รวมแจ้งเตือน)</span>\r\n <span className=\"font-normal text-xs text-muted-foreground\">\r\n รับสรุปแจ้งเตือนวันละครั้ง แทนการแจ้งเตือนทันที (ลด Spam)\r\n </span>\r\n </Label>\r\n <Switch\r\n id=\"digest-mode\"\r\n checked={digestMode}\r\n onCheckedChange={setDigestMode}\r\n />\r\n </div>\r\n </CardContent>\r\n <CardFooter>\r\n <Button variant=\"outline\" onClick={() => toast.success('บันทึกการตั้งค่าแจ้งเตือนแล้ว')}>\r\n บันทึกการตั้งค่า\r\n </Button>\r\n </CardFooter>\r\n </Card>\r\n </TabsContent>\r\n </Tabs>\r\n </div>\r\n );\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\projects\\new\\page.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'error' is defined but never used.",
"line": 92,
"column": 14,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 92,
"endColumn": 19
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "// File: app/(dashboard)/projects/new/page.tsx\r\n\"use client\";\r\n\r\nimport { useState } from \"react\";\r\nimport { useRouter } from \"next/navigation\";\r\nimport { useForm } from \"react-hook-form\";\r\nimport { zodResolver } from \"@hookform/resolvers/zod\";\r\nimport * as z from \"zod\";\r\nimport { Loader2, ChevronLeft, Save } from \"lucide-react\";\r\n\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport {\r\n Card,\r\n CardContent,\r\n CardDescription,\r\n CardFooter,\r\n CardHeader,\r\n CardTitle,\r\n} from \"@/components/ui/card\";\r\nimport { toast } from \"sonner\";\r\n\r\n// Force dynamic rendering to prevent build-time prerendering issues\r\nexport const dynamic = 'force-dynamic';\r\n\r\n// Ensure this page is never statically generated\r\nexport const fetchCache = 'force-no-store';\r\n\r\n// 1. กำหนด Schema สำหรับตรวจสอบข้อมูล (Validation)\r\n// อ้างอิงจาก Data Dictionary ตาราง projects\r\nconst projectSchema = z.object({\r\n projectCode: z\r\n .string()\r\n .min(1, \"กรุณาระบุรหัสโครงการ\")\r\n .max(50, \"รหัสโครงการต้องไม่เกิน 50 ตัวอักษร\")\r\n .regex(/^[A-Z0-9-]+$/, \"รหัสโครงการควรประกอบด้วยตัวอักษรภาษาอังกฤษตัวใหญ่ ตัวเลข หรือขีด (-) เท่านั้น\"),\r\n projectName: z\r\n .string()\r\n .min(1, \"กรุณาระบุชื่อโครงการ\")\r\n .max(255, \"ชื่อโครงการต้องไม่เกิน 255 ตัวอักษร\"),\r\n description: z.string().optional(),\r\n status: z.enum([\"Active\", \"Inactive\", \"On Hold\"]),\r\n startDate: z.string().optional(),\r\n endDate: z.string().optional(),\r\n});\r\n\r\ntype ProjectValues = z.infer<typeof projectSchema>;\r\n\r\nexport default function CreateProjectPage() {\r\n const router = useRouter();\r\n const [isLoading, setIsLoading] = useState(false);\r\n\r\n // 2. ตั้งค่า React Hook Form\r\n const {\r\n register,\r\n handleSubmit,\r\n setValue, // ใช้สำหรับ manual set value (เช่น Select)\r\n formState: { errors },\r\n } = useForm<ProjectValues>({\r\n resolver: zodResolver(projectSchema),\r\n defaultValues: {\r\n projectCode: \"\",\r\n projectName: \"\",\r\n status: \"Active\",\r\n },\r\n });\r\n\r\n // 3. ฟังก์ชัน Submit\r\n async function onSubmit(_data: ProjectValues) {\r\n setIsLoading(true);\r\n try {\r\n // เรียก API สร้างโครงการ (Mockup URL)\r\n // ใน Phase หลัง Backend จะเตรียม Endpoint POST /projects ไว้ให้\r\n\r\n // จำลองการส่งข้อมูล (Artificial Delay)\r\n await new Promise((resolve) => setTimeout(resolve, 1000));\r\n\r\n // await apiClient.post(\"/projects\", data);\r\n\r\n toast.success('สร้างโครงการสำเร็จ');\r\n router.push('/projects');\r\n router.refresh();\r\n } catch (error) {\r\n toast.error('เกิดข้อผิดพลาดในการสร้างโครงการ');\r\n // Project creation failed - toast shown\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n }\r\n\r\n return (\r\n <div className=\"max-w-2xl mx-auto space-y-6\">\r\n {/* Header with Back Button */}\r\n <div className=\"flex items-center gap-4\">\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n onClick={() => router.back()}\r\n className=\"h-9 w-9\"\r\n >\r\n <ChevronLeft className=\"h-5 w-5\" />\r\n <span className=\"sr-only\">Back</span>\r\n </Button>\r\n <div>\r\n <h2 className=\"text-2xl font-bold tracking-tight\">Create New Project</h2>\r\n <p className=\"text-muted-foreground\">\r\n สร้างโครงการใหม่เพื่อเริ่มบริหารจัดการสัญญาและเอกสาร\r\n </p>\r\n </div>\r\n </div>\r\n\r\n <form onSubmit={handleSubmit(onSubmit)}>\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>ข้อมูลโครงการ</CardTitle>\r\n <CardDescription>\r\n กรอกรายละเอียดสำคัญของโครงการ รหัสโครงการควรไม่ซ้ำกับที่มีอยู่\r\n </CardDescription>\r\n </CardHeader>\r\n\r\n <CardContent className=\"space-y-4\">\r\n {/* Project Code */}\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"project_code\" className=\"after:content-['*'] after:ml-0.5 after:text-red-500\">\r\n รหัสโครงการ (Project Code)\r\n </Label>\r\n <Input\r\n id=\"project_code\"\r\n placeholder=\"e.g. LCBP3-C1\"\r\n className={errors.projectCode ? \"border-destructive\" : \"\"}\r\n {...register(\"projectCode\")}\r\n onChange={(e) => {\r\n e.target.value = e.target.value.toUpperCase();\r\n register(\"projectCode\").onChange(e);\r\n }}\r\n />\r\n {errors.projectCode ? (\r\n <p className=\"text-xs text-destructive\">{errors.projectCode.message}</p>\r\n ) : (\r\n <p className=\"text-xs text-muted-foreground\">\r\n ใช้ภาษาอังกฤษตัวพิมพ์ใหญ่ ตัวเลข และขีด (-) เท่านั้น\r\n </p>\r\n )}\r\n </div>\r\n\r\n {/* Project Name */}\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"project_name\" className=\"after:content-['*'] after:ml-0.5 after:text-red-500\">\r\n ชื่อโครงการ (Project Name)\r\n </Label>\r\n <Input\r\n id=\"project_name\"\r\n placeholder=\"ระบุชื่อโครงการฉบับเต็ม...\"\r\n className={errors.projectName ? \"border-destructive\" : \"\"}\r\n {...register(\"projectName\")}\r\n />\r\n {errors.projectName && (\r\n <p className=\"text-xs text-destructive\">{errors.projectName.message}</p>\r\n )}\r\n </div>\r\n\r\n {/* Description */}\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"description\">รายละเอียดเพิ่มเติม</Label>\r\n <Textarea\r\n id=\"description\"\r\n placeholder=\"คำอธิบายเกี่ยวกับขอบเขตงานของโครงการ...\"\r\n className=\"min-h-[100px]\"\r\n {...register(\"description\")}\r\n />\r\n </div>\r\n\r\n {/* Dates Row */}\r\n <div className=\"grid gap-4 md:grid-cols-2\">\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"start_date\">วันที่เริ่มต้นสัญญา</Label>\r\n <Input\r\n id=\"start_date\"\r\n type=\"date\"\r\n {...register(\"startDate\")}\r\n />\r\n </div>\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"end_date\">วันที่สิ้นสุดสัญญา</Label>\r\n <Input\r\n id=\"end_date\"\r\n type=\"date\"\r\n {...register(\"endDate\")}\r\n />\r\n </div>\r\n </div>\r\n\r\n {/* Status Select */}\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"status\">สถานะโครงการ</Label>\r\n {/* เนื่องจาก Select ของ Shadcn เป็น Custom UI\r\n เราต้องใช้ onValueChange เพื่อเชื่อมกับ React Hook Form\r\n */}\r\n <Select\r\n onValueChange={(value: 'Active' | 'Inactive' | 'On Hold') => setValue('status', value)}\r\n defaultValue=\"Active\"\r\n >\r\n <SelectTrigger>\r\n <SelectValue placeholder=\"เลือกสถานะ\" />\r\n </SelectTrigger>\r\n <SelectContent>\r\n <SelectItem value=\"Active\">Active (กำลังดำเนินงาน)</SelectItem>\r\n <SelectItem value=\"On Hold\">On Hold (ระงับชั่วคราว)</SelectItem>\r\n <SelectItem value=\"Inactive\">Inactive (ปิดโครงการ)</SelectItem>\r\n </SelectContent>\r\n </Select>\r\n </div>\r\n </CardContent>\r\n\r\n <CardFooter className=\"flex justify-end gap-2 border-t p-4 bg-muted/50\">\r\n <Button\r\n type=\"button\"\r\n variant=\"ghost\"\r\n onClick={() => router.back()}\r\n disabled={isLoading}\r\n >\r\n ยกเลิก\r\n </Button>\r\n <Button type=\"submit\" disabled={isLoading}>\r\n {isLoading && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\r\n <Save className=\"mr-2 h-4 w-4\" />\r\n บันทึกข้อมูล\r\n </Button>\r\n </CardFooter>\r\n </Card>\r\n </form>\r\n </div>\r\n );\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\projects\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\projects\\page_backup.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\rfas\\[uuid]\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\rfas\\new\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\rfas\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\search\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\transmittals\\[uuid]\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\transmittals\\new\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\(dashboard)\\transmittals\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\api\\auth\\[...nextauth]\\route.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\error.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\global-error.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\layout.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\app\\page.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\admin\\organization-dialog.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\admin\\reference\\generic-crud-table.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\admin\\security\\rbac-matrix.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\admin\\sidebar.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\admin\\user-dialog.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\auth\\auth-sync.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\circulation\\circulation-list.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'StatusBadge' is defined but never used. Allowed unused vars must match /^_/u.",
"line": 6,
"column": 10,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 6,
"endColumn": 21,
"suggestions": [
{
"messageId": "removeUnusedImportDeclaration",
"data": { "varName": "StatusBadge" },
"fix": { "range": [202, 266], "text": "" },
"desc": "Remove unused import declaration."
}
]
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "\"use client\";\n\nimport { Circulation, CirculationListResponse } from \"@/types/circulation\";\nimport { DataTable } from \"@/components/common/data-table\";\nimport { ColumnDef } from \"@tanstack/react-table\";\nimport { StatusBadge } from \"@/components/common/status-badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Eye, _CheckCircle2 } from \"lucide-react\";\nimport Link from \"next/link\";\nimport { format } from \"date-fns\";\nimport { Badge } from \"@/components/ui/badge\";\n\ninterface CirculationListProps {\n data: CirculationListResponse;\n}\n\n/**\n * Calculate progress of circulation routings\n */\nfunction getProgress(routings?: Circulation[\"routings\"]) {\n if (!routings || routings.length === 0) return { completed: 0, total: 0 };\n const completed = routings.filter((r) => r.status === \"COMPLETED\").length;\n return { completed, total: routings.length };\n}\n\n/**\n * Get status color variant for circulation status\n */\nfunction getStatusVariant(\n statusCode: string\n): \"default\" | \"secondary\" | \"destructive\" | \"outline\" {\n switch (statusCode?.toUpperCase()) {\n case \"DRAFT\":\n return \"outline\";\n case \"ACTIVE\":\n case \"IN_PROGRESS\":\n return \"default\";\n case \"COMPLETED\":\n case \"CLOSED\":\n return \"secondary\";\n default:\n return \"outline\";\n }\n}\n\nexport function CirculationList({ data }: CirculationListProps) {\n if (!data) return null;\n\n const columns: ColumnDef<Circulation>[] = [\n {\n accessorKey: \"circulationNo\",\n header: \"Circulation No.\",\n cell: ({ row }) => (\n <span className=\"font-medium\">{row.getValue(\"circulationNo\")}</span>\n ),\n },\n {\n accessorKey: \"subject\",\n header: \"Subject\",\n cell: ({ row }) => (\n <div className=\"max-w-[250px] truncate\" title={row.getValue(\"subject\")}>\n {row.getValue(\"subject\")}\n </div>\n ),\n },\n {\n accessorKey: \"organization\",\n header: \"Organization\",\n cell: ({ row }) => {\n const org = row.original.organization;\n return org?.organization_name || \"-\";\n },\n },\n {\n accessorKey: \"statusCode\",\n header: \"Status\",\n cell: ({ row }) => {\n const status = row.getValue(\"statusCode\") as string;\n return <Badge variant={getStatusVariant(status)}>{status}</Badge>;\n },\n },\n {\n id: \"progress\",\n header: \"Progress\",\n cell: ({ row }) => {\n const { completed, total } = getProgress(row.original.routings);\n if (total === 0) return \"-\";\n const percent = Math.round((completed / total) * 100);\n return (\n <div className=\"flex items-center gap-2\">\n <div className=\"w-20 h-2 bg-muted rounded-full overflow-hidden\">\n <div\n className=\"h-full bg-primary transition-all\"\n style={{ width: `${percent}%` }}\n />\n </div>\n <span className=\"text-xs text-muted-foreground\">\n {completed}/{total}\n </span>\n </div>\n );\n },\n },\n {\n accessorKey: \"createdAt\",\n header: \"Created\",\n cell: ({ row }) =>\n format(new Date(row.getValue(\"createdAt\")), \"dd MMM yyyy\"),\n },\n {\n id: \"actions\",\n cell: ({ row }) => {\n const item = row.original;\n return (\n <div className=\"flex gap-1\">\n <Link href={`/circulation/${item.uuid}`}>\n <Button variant=\"ghost\" size=\"icon\" title=\"View Details\">\n <Eye className=\"h-4 w-4\" />\n </Button>\n </Link>\n </div>\n );\n },\n },\n ];\n\n return (\n <div>\n <DataTable columns={columns} data={data.data || []} />\n {data.meta && (\n <div className=\"mt-4 text-sm text-muted-foreground text-center\">\n Showing {data.data?.length || 0} of {data.meta.total} circulations\n </div>\n )}\n </div>\n );\n}\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\common\\can.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\common\\confirm-dialog.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\common\\data-table.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\common\\pagination.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\common\\status-badge.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\correspondences\\correspondences-content.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\correspondences\\detail.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\correspondences\\form.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 86,
"column": 75,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 86,
"endColumn": 78,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [2912, 2915], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [2912, 2915], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 102,
"column": 55,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 102,
"endColumn": 58,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [3877, 3880], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [3877, 3880], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 117,
"column": 57,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 117,
"endColumn": 60,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [5082, 5085], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [5082, 5085], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 118,
"column": 48,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 118,
"endColumn": 51,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [5189, 5192], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [5189, 5192], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 131,
"column": 52,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 131,
"endColumn": 55,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [5570, 5573], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [5570, 5573], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
]
},
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'err' is defined but never used.",
"line": 198,
"column": 17,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 198,
"endColumn": 20
}
],
"suppressedMessages": [],
"errorCount": 6,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "\"use client\";\r\n\r\nimport { useForm } from \"react-hook-form\";\r\nimport { zodResolver } from \"@hookform/resolvers/zod\";\r\nimport { z } from \"zod\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport { FileUploadZone } from \"@/components/custom/file-upload-zone\";\r\nimport { useRouter } from \"next/navigation\";\r\nimport { Loader2 } from \"lucide-react\";\r\nimport { useCreateCorrespondence, useUpdateCorrespondence } from \"@/hooks/use-correspondence\";\r\nimport { Organization } from \"@/types/organization\";\r\nimport { useOrganizations, useProjects, useCorrespondenceTypes, useDisciplines } from \"@/hooks/use-master-data\";\r\nimport { CreateCorrespondenceDto } from \"@/types/dto/correspondence/create-correspondence.dto\";\r\nimport { useState, useEffect } from \"react\";\r\nimport { _correspondenceService } from \"@/lib/services/correspondence.service\";\r\nimport { numberingApi } from \"@/lib/api/numbering\";\r\n\r\n// Updated Zod Schema with all required fields\r\nconst correspondenceSchema = z.object({\r\n projectId: z.string().min(1, \"Please select a Project\"),\r\n documentTypeId: z.number().min(1, \"Please select a Document Type\"),\r\n disciplineId: z.number().optional(),\r\n subject: z.string().min(5, \"Subject must be at least 5 characters\"),\r\n description: z.string().optional(),\r\n body: z.string().optional(),\r\n remarks: z.string().optional(),\r\n dueDate: z.string().optional(), // ISO Date string\r\n documentDate: z.string().optional(),\r\n issuedDate: z.string().optional(),\r\n receivedDate: z.string().optional(),\r\n fromOrganizationId: z.string().min(1, \"Please select From Organization\"),\r\n toOrganizationId: z.string().min(1, \"Please select To Organization\"),\r\n importance: z.enum([\"NORMAL\", \"HIGH\", \"URGENT\"]),\r\n attachments: z.array(z.instanceof(File)).optional(),\r\n});\r\n\r\ntype FormData = z.infer<typeof correspondenceSchema>;\r\n\r\ntype ProjectOption = {\r\n uuid?: string;\r\n id?: number;\r\n projectName: string;\r\n projectCode: string;\r\n};\r\n\r\ntype CorrespondenceTypeOption = {\r\n id: number;\r\n typeName: string;\r\n typeCode: string;\r\n};\r\n\r\ntype DisciplineOption = {\r\n id: number;\r\n disciplineCode: string;\r\n codeNameEn?: string;\r\n};\r\n\r\nconst extractArrayData = <T,>(value: unknown): T[] => {\r\n let current: unknown = value;\r\n\r\n for (let i = 0; i < 5; i += 1) {\r\n if (Array.isArray(current)) {\r\n return current as T[];\r\n }\r\n\r\n if (!current || typeof current !== \"object\" || !(\"data\" in current)) {\r\n return [];\r\n }\r\n\r\n current = (current as { data?: unknown }).data;\r\n }\r\n\r\n return Array.isArray(current) ? (current as T[]) : [];\r\n};\r\n\r\nexport function CorrespondenceForm({ initialData, uuid }: { initialData?: any, uuid?: string }) {\r\n const router = useRouter();\r\n const createMutation = useCreateCorrespondence();\r\n const updateMutation = useUpdateCorrespondence();\r\n\r\n // Fetch master data for dropdowns\r\n const { data: projectsData, isLoading: isLoadingProjects } = useProjects();\r\n const { data: organizations, isLoading: isLoadingOrgs } = useOrganizations();\r\n const { data: correspondenceTypesData, isLoading: isLoadingTypes } = useCorrespondenceTypes();\r\n const { data: disciplinesData, isLoading: isLoadingDisciplines } = useDisciplines();\r\n const projects = extractArrayData<ProjectOption>(projectsData);\r\n const organizationOptions = extractArrayData<Organization>(organizations);\r\n const correspondenceTypes = extractArrayData<CorrespondenceTypeOption>(correspondenceTypesData);\r\n const disciplines = extractArrayData<DisciplineOption>(disciplinesData);\r\n\r\n // Extract initial values if editing\r\n const currentRev = initialData?.revisions?.find((r: any) => r.isCurrent) || initialData?.revisions?.[0];\r\n const defaultValues: Partial<FormData> = {\r\n projectId: initialData?.project?.uuid || (initialData?.projectId ? String(initialData.projectId) : undefined),\r\n documentTypeId: initialData?.correspondenceTypeId || undefined,\r\n disciplineId: initialData?.disciplineId || undefined,\r\n subject: currentRev?.subject || currentRev?.title || \"\",\r\n description: currentRev?.description || \"\",\r\n body: currentRev?.body || \"\",\r\n remarks: currentRev?.remarks || \"\",\r\n dueDate: currentRev?.dueDate ? new Date(currentRev.dueDate).toISOString().split('T')[0] : undefined,\r\n documentDate: currentRev?.documentDate ? new Date(currentRev.documentDate).toISOString().split('T')[0] : undefined,\r\n issuedDate: currentRev?.issuedDate ? new Date(currentRev.issuedDate).toISOString().split('T')[0] : undefined,\r\n receivedDate: currentRev?.receivedDate ? new Date(currentRev.receivedDate).toISOString().split('T')[0] : undefined,\r\n fromOrganizationId: initialData?.originatorId ? String(initialData.originatorId) : undefined,\r\n // Map initial recipient (TO) - Simplified for now\r\n toOrganizationId: initialData?.recipients?.find((r: any) => r.recipientType === 'TO')?.recipientOrganizationId\r\n ? String(initialData.recipients.find((r: any) => r.recipientType === 'TO').recipientOrganizationId)\r\n : undefined,\r\n importance: currentRev?.details?.importance || \"NORMAL\",\r\n };\r\n\r\n const {\r\n register,\r\n handleSubmit,\r\n setValue,\r\n watch,\r\n formState: { errors },\r\n } = useForm<FormData>({\r\n // @ts-ignore: Zod version mismatch in monorepo\r\n resolver: zodResolver(correspondenceSchema) as any,\r\n defaultValues: defaultValues as FormData,\r\n });\r\n\r\n // Watch for controlled inputs\r\n const projectId = watch(\"projectId\");\r\n const documentTypeId = watch(\"documentTypeId\");\r\n const disciplineId = watch(\"disciplineId\");\r\n const fromOrgId = watch(\"fromOrganizationId\");\r\n const toOrgId = watch(\"toOrganizationId\");\r\n\r\n const onSubmit = (data: FormData) => {\r\n const payload: CreateCorrespondenceDto = {\r\n projectId: data.projectId,\r\n typeId: data.documentTypeId,\r\n disciplineId: data.disciplineId,\r\n subject: data.subject,\r\n description: data.description,\r\n body: data.body,\r\n remarks: data.remarks,\r\n dueDate: data.dueDate ? new Date(data.dueDate).toISOString() : undefined,\r\n documentDate: data.documentDate ? new Date(data.documentDate).toISOString() : undefined,\r\n issuedDate: data.issuedDate ? new Date(data.issuedDate).toISOString() : undefined,\r\n receivedDate: data.receivedDate ? new Date(data.receivedDate).toISOString() : undefined,\r\n originatorId: data.fromOrganizationId,\r\n recipients: [\r\n { organizationId: data.toOrganizationId, type: 'TO' }\r\n ],\r\n details: {\r\n importance: data.importance\r\n },\r\n };\r\n\r\n if (uuid && initialData) {\r\n // UPDATE Mode\r\n updateMutation.mutate({ uuid, data: payload }, {\r\n onSuccess: () => router.push(`/correspondences/${uuid}`)\r\n });\r\n } else {\r\n // CREATE Mode\r\n createMutation.mutate(payload, {\r\n onSuccess: () => router.push(\"/correspondences\"),\r\n });\r\n }\r\n };\r\n\r\n const isPending = createMutation.isPending || updateMutation.isPending;\r\n\r\n // -- Preview Logic --\r\n const [preview, setPreview] = useState<{ number: string; isDefaultTemplate: boolean } | null>(null);\r\n\r\n useEffect(() => {\r\n if (!projectId || !documentTypeId || !fromOrgId || !toOrgId) {\r\n setPreview(null);\r\n return;\r\n }\r\n\r\n const fetchPreview = async () => {\r\n try {\r\n const res = await numberingApi.previewNumber({\r\n projectId,\r\n correspondenceTypeId: documentTypeId,\r\n disciplineId,\r\n originatorOrganizationId: fromOrgId,\r\n recipientOrganizationId: toOrgId\r\n });\r\n setPreview({ number: res.previewNumber, isDefaultTemplate: res.isDefault });\r\n } catch (err) {\r\n setPreview(null);\r\n }\r\n };\r\n\r\n const timer = setTimeout(fetchPreview, 500);\r\n return () => clearTimeout(timer);\r\n }, [projectId, documentTypeId, disciplineId, fromOrgId, toOrgId]);\r\n\r\n\r\n\r\n return (\r\n <form onSubmit={handleSubmit(onSubmit)} className=\"max-w-3xl space-y-6\">\r\n {/* Existing Document Number (Read Only) */}\r\n {initialData?.correspondenceNumber && (\r\n <div className=\"space-y-2\">\r\n <Label>Current Document Number</Label>\r\n <div className=\"flex items-center gap-2\">\r\n <Input value={initialData.correspondenceNumber} disabled readOnly className=\"bg-muted font-mono font-bold text-lg w-full\" />\r\n {preview && preview.number !== initialData.correspondenceNumber && (\r\n <span className=\"text-xs text-amber-600 font-semibold whitespace-nowrap px-2\">\r\n Start Change Detected\r\n </span>\r\n )}\r\n </div>\r\n </div>\r\n )}\r\n\r\n {/* Preview Section */}\r\n {preview && (\r\n <div className={`p-4 rounded-md border ${preview.number !== initialData?.correspondenceNumber ? 'bg-amber-50 border-amber-200' : 'bg-muted border-border'}`}>\r\n <p className=\"text-sm font-semibold mb-1 flex items-center gap-2\">\r\n {initialData?.correspondenceNumber ? \"New Document Number (Preview)\" : \"Document Number Preview\"}\r\n {preview.number !== initialData?.correspondenceNumber && initialData?.correspondenceNumber && (\r\n <span className=\"text-[10px] bg-amber-100 text-amber-700 px-2 py-0.5 rounded-full border border-amber-200\">\r\n Will Update\r\n </span>\r\n )}\r\n </p>\r\n <div className=\"flex items-center gap-3\">\r\n <span className={`text-xl font-bold font-mono tracking-wide ${preview.number !== initialData?.correspondenceNumber ? 'text-amber-700' : 'text-primary'}`}>\r\n {preview.number}\r\n </span>\r\n {preview.isDefaultTemplate && (\r\n <span className=\"text-[10px] uppercase font-semibold px-2 py-0.5 rounded-full bg-yellow-100 text-yellow-800 border border-yellow-200\">\r\n Default Template\r\n </span>\r\n )}\r\n </div>\r\n {preview.number !== initialData?.correspondenceNumber && initialData?.correspondenceNumber && (\r\n <p className=\"text-xs text-muted-foreground mt-2\">\r\n * The document number will be regenerated because critical fields were changed.\r\n </p>\r\n )}\r\n </div>\r\n )}\r\n\r\n {/* Document Metadata Section */}\r\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\r\n {/* Project Dropdown */}\r\n <div className=\"space-y-2\">\r\n <Label>Project *</Label>\r\n <Select\r\n onValueChange={(v) => setValue(\"projectId\", v)}\r\n value={projectId || undefined}\r\n disabled={isLoadingProjects}\r\n >\r\n <SelectTrigger>\r\n <SelectValue placeholder={isLoadingProjects ? \"Loading...\" : \"Select Project\"} />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {projects.map((p) => (\r\n <SelectItem key={p.uuid || String(p.id)} value={p.uuid || String(p.id)}>\r\n {p.projectName} ({p.projectCode})\r\n </SelectItem>\r\n ))}\r\n </SelectContent>\r\n </Select>\r\n {errors.projectId && (\r\n <p className=\"text-sm text-destructive\">{errors.projectId.message}</p>\r\n )}\r\n </div>\r\n\r\n {/* Document Type Dropdown */}\r\n <div className=\"space-y-2\">\r\n <Label>Document Type *</Label>\r\n <Select\r\n onValueChange={(v) => setValue(\"documentTypeId\", Number(v))}\r\n value={documentTypeId ? String(documentTypeId) : undefined}\r\n disabled={isLoadingTypes}\r\n >\r\n <SelectTrigger>\r\n <SelectValue placeholder={isLoadingTypes ? \"Loading...\" : \"Select Type\"} />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {correspondenceTypes.map((t) => (\r\n <SelectItem key={t.id} value={String(t.id)}>\r\n {t.typeName} ({t.typeCode})\r\n </SelectItem>\r\n ))}\r\n </SelectContent>\r\n </Select>\r\n {errors.documentTypeId && (\r\n <p className=\"text-sm text-destructive\">{errors.documentTypeId.message}</p>\r\n )}\r\n </div>\r\n\r\n {/* Discipline Dropdown (Optional) */}\r\n <div className=\"space-y-2\">\r\n <Label>Discipline</Label>\r\n <Select\r\n onValueChange={(v) => setValue(\"disciplineId\", v ? Number(v) : undefined)}\r\n value={disciplineId ? String(disciplineId) : undefined}\r\n disabled={isLoadingDisciplines}\r\n >\r\n <SelectTrigger>\r\n <SelectValue placeholder={isLoadingDisciplines ? \"Loading...\" : \"Select Discipline (Optional)\"} />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {disciplines.map((d) => (\r\n <SelectItem key={d.id} value={String(d.id)}>\r\n {d.codeNameEn || d.disciplineCode}\r\n </SelectItem>\r\n ))}\r\n </SelectContent>\r\n </Select>\r\n </div>\r\n </div>\r\n\r\n {/* Subject */}\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"subject\">Subject *</Label>\r\n <Input id=\"subject\" {...register(\"subject\")} placeholder=\"Enter subject\" />\r\n {errors.subject && (\r\n <p className=\"text-sm text-destructive\">{errors.subject.message}</p>\r\n )}\r\n </div>\r\n\r\n {/* Body */}\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"body\">Body (Content)</Label>\r\n <Textarea\r\n id=\"body\"\r\n {...register(\"body\")}\r\n rows={6}\r\n placeholder=\"Enter letter content...\"\r\n />\r\n </div>\r\n\r\n {/* Date Fields */}\r\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4\">\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"documentDate\">Document Date</Label>\r\n <Input \r\n id=\"documentDate\" \r\n type=\"date\" \r\n {...register(\"documentDate\")} \r\n onChange={(e) => {\r\n const val = e.target.value;\r\n setValue(\"documentDate\", val, { shouldValidate: true, shouldDirty: true });\r\n if (val) {\r\n setValue(\"issuedDate\", val, { shouldValidate: true, shouldDirty: true });\r\n setValue(\"receivedDate\", val, { shouldValidate: true, shouldDirty: true });\r\n const d = new Date(val);\r\n d.setDate(d.getDate() + 7);\r\n setValue(\"dueDate\", d.toISOString().split('T')[0], { shouldValidate: true, shouldDirty: true });\r\n }\r\n }}\r\n />\r\n </div>\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"issuedDate\">Issued Date</Label>\r\n <Input id=\"issuedDate\" type=\"date\" {...register(\"issuedDate\")} />\r\n </div>\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"receivedDate\">Received Date</Label>\r\n <Input \r\n id=\"receivedDate\" \r\n type=\"date\" \r\n {...register(\"receivedDate\")} \r\n onChange={(e) => {\r\n const val = e.target.value;\r\n setValue(\"receivedDate\", val, { shouldValidate: true, shouldDirty: true });\r\n if (val) {\r\n const d = new Date(val);\r\n d.setDate(d.getDate() + 7);\r\n setValue(\"dueDate\", d.toISOString().split('T')[0], { shouldValidate: true, shouldDirty: true });\r\n }\r\n }}\r\n />\r\n </div>\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"dueDate\">Due Date</Label>\r\n <Input id=\"dueDate\" type=\"date\" {...register(\"dueDate\")} />\r\n </div>\r\n </div>\r\n\r\n {/* Remarks */}\r\n <div className=\"grid grid-cols-1 gap-4\">\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"remarks\">Remarks</Label>\r\n <Input id=\"remarks\" {...register(\"remarks\")} placeholder=\"Optional remarks\" />\r\n </div>\r\n </div>\r\n\r\n {/* Description */}\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"description\">Description (Internal Note)</Label>\r\n <Textarea\r\n id=\"description\"\r\n {...register(\"description\")}\r\n rows={2}\r\n placeholder=\"Enter description...\"\r\n />\r\n </div>\r\n\r\n {/* Organizations */}\r\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\r\n <div className=\"space-y-2\">\r\n <Label>From Organization *</Label>\r\n <Select\r\n onValueChange={(v) => setValue(\"fromOrganizationId\", v)}\r\n value={fromOrgId || undefined}\r\n disabled={isLoadingOrgs}\r\n >\r\n <SelectTrigger>\r\n <SelectValue placeholder={isLoadingOrgs ? \"Loading...\" : \"Select Organization\"} />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {organizationOptions.map((org) => (\r\n <SelectItem key={org.uuid} value={org.uuid}>\r\n {org.organizationName} ({org.organizationCode})\r\n </SelectItem>\r\n ))}\r\n </SelectContent>\r\n </Select>\r\n {errors.fromOrganizationId && (\r\n <p className=\"text-sm text-destructive\">{errors.fromOrganizationId.message}</p>\r\n )}\r\n </div>\r\n\r\n <div className=\"space-y-2\">\r\n <Label>To Organization *</Label>\r\n <Select\r\n onValueChange={(v) => setValue(\"toOrganizationId\", v)}\r\n value={toOrgId || undefined}\r\n disabled={isLoadingOrgs}\r\n >\r\n <SelectTrigger>\r\n <SelectValue placeholder={isLoadingOrgs ? \"Loading...\" : \"Select Organization\"} />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {organizationOptions.map((org) => (\r\n <SelectItem key={org.uuid} value={org.uuid}>\r\n {org.organizationName} ({org.organizationCode})\r\n </SelectItem>\r\n ))}\r\n </SelectContent>\r\n </Select>\r\n {errors.toOrganizationId && (\r\n <p className=\"text-sm text-destructive\">{errors.toOrganizationId.message}</p>\r\n )}\r\n </div>\r\n </div>\r\n\r\n {/* Importance */}\r\n <div className=\"space-y-2\">\r\n <Label>Importance</Label>\r\n <div className=\"flex gap-6 mt-2\">\r\n <label className=\"flex items-center space-x-2 cursor-pointer\">\r\n <input\r\n type=\"radio\"\r\n value=\"NORMAL\"\r\n {...register(\"importance\")}\r\n className=\"accent-primary\"\r\n />\r\n <span>Normal</span>\r\n </label>\r\n <label className=\"flex items-center space-x-2 cursor-pointer\">\r\n <input\r\n type=\"radio\"\r\n value=\"HIGH\"\r\n {...register(\"importance\")}\r\n className=\"accent-primary\"\r\n />\r\n <span>High</span>\r\n </label>\r\n <label className=\"flex items-center space-x-2 cursor-pointer\">\r\n <input\r\n type=\"radio\"\r\n value=\"URGENT\"\r\n {...register(\"importance\")}\r\n className=\"accent-primary\"\r\n />\r\n <span>Urgent</span>\r\n </label>\r\n </div>\r\n </div>\r\n\r\n {/* Attachments (only for new documents) */}\r\n {!initialData && (\r\n <div className=\"space-y-2\">\r\n <Label>Attachments</Label>\r\n <FileUploadZone\r\n onFilesChanged={(files) => setValue(\"attachments\", files)}\r\n multiple\r\n accept={[\".pdf\", \".doc\", \".docx\", \".xls\", \".xlsx\", \".jpg\", \".png\"]}\r\n />\r\n </div>\r\n )}\r\n\r\n {/* Actions */}\r\n <div className=\"flex justify-end gap-4 pt-6 border-t\">\r\n <Button type=\"button\" variant=\"outline\" onClick={() => router.back()}>\r\n Cancel\r\n </Button>\r\n <Button type=\"submit\" disabled={isPending}>\r\n {isPending && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\r\n {uuid ? \"Update Correspondence\" : \"Create Correspondence\"}\r\n </Button>\r\n </div>\r\n </form>\r\n );\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\correspondences\\list.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\custom\\file-upload-zone.tsx",
"messages": [
{
"ruleId": "react-hooks/exhaustive-deps",
"severity": 1,
"message": "React Hook useCallback has a missing dependency: 'validateFile'. Either include it or remove the dependency array.",
"line": 88,
"column": 5,
"nodeType": "ArrayExpression",
"endLine": 88,
"endColumn": 48,
"suggestions": [
{
"desc": "Update the dependencies array to be: [validateFile, multiple, onFilesChanged]",
"fix": { "range": [2956, 2999], "text": "[validateFile, multiple, onFilesChanged]" }
}
]
}
],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 1,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "// File: components/custom/file-upload-zone.tsx\r\n\r\n\"use client\";\r\n\r\nimport React, { useCallback, useState } from \"react\";\r\nimport { UploadCloud, File as FileIcon, X, AlertTriangle, CheckCircle } from \"lucide-react\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\r\n\r\nexport interface FileWithMeta extends File {\r\n preview?: string;\r\n validationError?: string;\r\n}\r\n\r\ninterface FileUploadZoneProps {\r\n /** Callback เมื่อไฟล์มีการเปลี่ยนแปลง */\r\n onFilesChanged: (files: FileWithMeta[]) => void;\r\n /** ประเภทไฟล์ที่ยอมรับ (เช่น .pdf, .dwg) */\r\n accept?: string[];\r\n /** ขนาดไฟล์สูงสุด (Bytes) Default: 50MB */\r\n maxSize?: number;\r\n /** อนุญาตให้อัปโหลดหลายไฟล์หรือไม่ */\r\n multiple?: boolean;\r\n /** ไฟล์ที่มีอยู่เดิม (ถ้ามี) */\r\n initialFiles?: FileWithMeta[];\r\n className?: string;\r\n}\r\n\r\n/**\r\n * Helper: แปลง Bytes เป็นหน่วยที่อ่านง่าย\r\n */\r\nconst formatBytes = (bytes: number, decimals = 2) => {\r\n if (!Number(bytes)) return \"0 Bytes\";\r\n const k = 1024;\r\n const dm = decimals < 0 ? 0 : decimals;\r\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\r\n const i = Math.floor(Math.log(bytes) / Math.log(k));\r\n return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;\r\n};\r\n\r\n/**\r\n * FileUploadZone Component\r\n * รองรับ Drag & Drop, Validation และแสดงรายการไฟล์\r\n */\r\nexport function FileUploadZone({\r\n onFilesChanged,\r\n accept = [\".pdf\", \".dwg\", \".docx\", \".xlsx\", \".zip\"],\r\n maxSize = 50 * 1024 * 1024, // 50MB Default\r\n multiple = true,\r\n initialFiles = [],\r\n className,\r\n}: FileUploadZoneProps) {\r\n const [files, setFiles] = useState<FileWithMeta[]>(initialFiles);\r\n const [isDragging, setIsDragging] = useState(false);\r\n\r\n // ตรวจสอบไฟล์\r\n const validateFile = (file: File): string | undefined => {\r\n // 1. Check Size\r\n if (file.size > maxSize) {\r\n return `ขนาดไฟล์เกินกำหนด (${formatBytes(maxSize)})`;\r\n }\r\n // 2. Check Type (Extension based validation for simplicity on client)\r\n const fileExtension = \".\" + file.name.split(\".\").pop()?.toLowerCase();\r\n if (accept.length > 0 && !accept.includes(fileExtension)) {\r\n return `ประเภทไฟล์ไม่รองรับ (อนุญาต: ${accept.join(\", \")})`;\r\n }\r\n return undefined;\r\n };\r\n\r\n const handleFileSelect = useCallback(\r\n (newFiles: File[]) => {\r\n const processedFiles: FileWithMeta[] = newFiles.map((file) => {\r\n const error = validateFile(file);\r\n // สร้าง Object ใหม่เพื่อไม่ให้กระทบ File object เดิม\r\n const fileWithMeta = new File([file], file.name, { type: file.type }) as FileWithMeta;\r\n fileWithMeta.validationError = error;\r\n return fileWithMeta;\r\n });\r\n\r\n setFiles((prev) => {\r\n const updated = multiple ? [...prev, ...processedFiles] : processedFiles;\r\n onFilesChanged(updated);\r\n return updated;\r\n });\r\n },\r\n [maxSize, accept, multiple, onFilesChanged]\r\n );\r\n\r\n // Drag Events\r\n const onDragOver = (e: React.DragEvent) => {\r\n e.preventDefault();\r\n setIsDragging(true);\r\n };\r\n const onDragLeave = (e: React.DragEvent) => {\r\n e.preventDefault();\r\n setIsDragging(false);\r\n };\r\n const onDrop = (e: React.DragEvent) => {\r\n e.preventDefault();\r\n setIsDragging(false);\r\n if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {\r\n handleFileSelect(Array.from(e.dataTransfer.files));\r\n }\r\n };\r\n\r\n const removeFile = (indexToRemove: number) => {\r\n setFiles((prev) => {\r\n const updated = prev.filter((_, index) => index !== indexToRemove);\r\n onFilesChanged(updated);\r\n return updated;\r\n });\r\n };\r\n\r\n return (\r\n <div className={cn(\"w-full space-y-4\", className)}>\r\n {/* Drop Zone */}\r\n <div\r\n className={cn(\r\n \"border-2 border-dashed rounded-lg p-8 text-center transition-colors cursor-pointer flex flex-col items-center justify-center gap-2\",\r\n isDragging\r\n ? \"border-primary bg-primary/10\"\r\n : \"border-muted-foreground/25 hover:border-primary/50\",\r\n \"h-48\"\r\n )}\r\n onDragOver={onDragOver}\r\n onDragLeave={onDragLeave}\r\n onDrop={onDrop}\r\n onClick={() => document.getElementById(\"file-input\")?.click()}\r\n >\r\n <input\r\n id=\"file-input\"\r\n type=\"file\"\r\n className=\"hidden\"\r\n multiple={multiple}\r\n accept={accept.join(\",\")}\r\n onChange={(e) => {\r\n if (e.target.files) handleFileSelect(Array.from(e.target.files));\r\n }}\r\n />\r\n <div className=\"p-3 bg-muted rounded-full\">\r\n <UploadCloud className=\"w-8 h-8 text-muted-foreground\" />\r\n </div>\r\n <div className=\"space-y-1\">\r\n <p className=\"text-sm font-medium\">\r\n คลิกเพื่อเลือกไฟล์ หรือ ลากไฟล์มาวางที่นี่\r\n </p>\r\n <p className=\"text-xs text-muted-foreground\">\r\n รองรับ: {accept.join(\", \")} (สูงสุด {formatBytes(maxSize)})\r\n </p>\r\n </div>\r\n </div>\r\n\r\n {/* File List */}\r\n {files.length > 0 && (\r\n <ScrollArea className=\"h-[200px] w-full rounded-md border p-4\">\r\n <div className=\"space-y-3\">\r\n {files.map((file, index) => (\r\n <div\r\n key={`${file.name}-${index}`}\r\n className=\"flex items-center justify-between p-3 bg-card rounded-md border shadow-sm group\"\r\n >\r\n <div className=\"flex items-center gap-3 overflow-hidden\">\r\n <div className=\"p-2 bg-primary/10 rounded-md shrink-0\">\r\n <FileIcon className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div className=\"min-w-0\">\r\n <p className=\"text-sm font-medium truncate max-w-[200px] sm:max-w-md\">\r\n {file.name}\r\n </p>\r\n <div className=\"flex items-center gap-2\">\r\n <span className=\"text-xs text-muted-foreground\">\r\n {formatBytes(file.size)}\r\n </span>\r\n {file.validationError ? (\r\n <Badge variant=\"destructive\" className=\"text-[10px] px-1 h-5 flex gap-1\">\r\n <AlertTriangle className=\"w-3 h-3\" /> {file.validationError}\r\n </Badge>\r\n ) : (\r\n <Badge variant=\"outline\" className=\"text-[10px] px-1 h-5 text-green-600 bg-green-50 border-green-200 flex gap-1\">\r\n <CheckCircle className=\"w-3 h-3\" /> Ready\r\n </Badge>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"shrink-0 text-muted-foreground hover:text-destructive\"\r\n onClick={() => removeFile(index)}\r\n >\r\n <X className=\"w-4 h-4\" />\r\n </Button>\r\n </div>\r\n ))}\r\n </div>\r\n </ScrollArea>\r\n )}\r\n </div>\r\n );\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\custom\\workflow-visualizer.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\dashboard\\pending-tasks.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\dashboard\\quick-actions.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\dashboard\\recent-activity.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\dashboard\\stats-cards.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\documents\\common\\server-data-table.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\drawings\\card.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\drawings\\columns.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\drawings\\list.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\drawings\\revision-history.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\drawings\\upload-form.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\layout\\dashboard-shell.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\layout\\global-search.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\layout\\header.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\layout\\navbar.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\layout\\notifications-dropdown.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\layout\\sidebar.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\layout\\user-menu.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\layout\\user-nav.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\numbering\\audit-logs-table.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'error' is defined but never used.",
"line": 26,
"column": 16,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 26,
"endColumn": 21
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/components/ui/table\";\nimport { documentNumberingService } from \"@/lib/services/document-numbering.service\";\nimport { format } from \"date-fns\";\n\nexport function AuditLogsTable() {\n const [logs, setLogs] = useState<unknown[]>([]); // Replace with AuditLog type\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n async function fetchLogs() {\n try {\n const data = await documentNumberingService.getMetrics(); // Using metrics endpoint for now as it contains logs\n if (data && data.audit) {\n setLogs(data.audit);\n }\n } catch (error) {\n // Failed to fetch audit logs - empty state shown\n } finally {\n setLoading(false);\n }\n }\n fetchLogs();\n }, []);\n\n if (loading) return <div>Loading logs...</div>;\n\n return (\n <div className=\"rounded-md border\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Time</TableHead>\n <TableHead>Operation</TableHead>\n <TableHead>Number</TableHead>\n <TableHead>User</TableHead>\n <TableHead>Status</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {logs.length === 0 ? (\n <TableRow>\n <TableCell colSpan={5} className=\"text-center\">No logs found.</TableCell>\n </TableRow>\n ) : (\n logs.map((log) => (\n <TableRow key={log.id}>\n <TableCell>{format(new Date(log.createdAt), \"yyyy-MM-dd HH:mm:ss\")}</TableCell>\n <TableCell>{log.operation}</TableCell>\n <TableCell>{log.generatedNumber}</TableCell>\n <TableCell>{log.createdBy || \"System\"}</TableCell>\n <TableCell>{log.status}</TableCell>\n </TableRow>\n ))\n )}\n </TableBody>\n </Table>\n </div>\n );\n}\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\numbering\\bulk-import-form.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'_error' is defined but never used.",
"line": 31,
"column": 14,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 31,
"endColumn": 20
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { toast } from \"sonner\";\nimport { documentNumberingService } from \"@/lib/services/document-numbering.service\";\n\nexport function BulkImportForm({ projectId = 1 }: { projectId?: number | string }) {\n const [file, setFile] = useState<File | null>(null);\n const [loading, setLoading] = useState(false);\n\n const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n if (e.target.files) {\n setFile(e.target.files[0]);\n }\n };\n\n const handleUpload = async () => {\n if (!file) return;\n setLoading(true);\n try {\n const formData = new FormData();\n formData.append(\"file\", file);\n formData.append(\"projectId\", projectId.toString());\n\n await documentNumberingService.bulkImport(formData);\n toast.success(\"Bulk import initiated. Check audit logs for progress.\");\n setFile(null);\n } catch (_error) {\n toast.error(\"Failed to import numbers.\");\n } finally {\n setLoading(false);\n }\n };\n\n return (\n <div className=\"border p-4 rounded-md space-y-4\">\n <h3 className=\"text-lg font-medium\">Bulk Import Numbers</h3>\n <p className=\"text-sm text-gray-500\">Import legacy numbers via CSV to reserve them in the system.</p>\n\n <div className=\"grid w-full max-w-sm items-center gap-1.5\">\n <Label htmlFor=\"csv-file\">CSV File</Label>\n <Input id=\"csv-file\" type=\"file\" accept=\".csv,.xlsx\" onChange={handleFileChange} />\n </div>\n\n <Button onClick={handleUpload} disabled={!file || loading}>\n {loading ? \"Importing...\" : \"Upload & Import\"}\n </Button>\n </div>\n );\n}\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\numbering\\cancel-number-form.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'_error' is defined but never used.",
"line": 46,
"column": 14,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 46,
"endColumn": 20
}
],
"suppressedMessages": [
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 32,
"column": 42,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 32,
"endColumn": 45,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [968, 971], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [968, 971], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
],
"suppressions": [{ "kind": "directive", "justification": "zod 4 + @hookform/resolvers compat" }]
}
],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "\"use client\";\n\nimport { useForm } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport * as z from \"zod\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Form,\n FormControl,\n FormField,\n FormItem,\n FormLabel,\n FormMessage\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { toast } from \"sonner\";\nimport { documentNumberingService } from \"@/lib/services/document-numbering.service\";\nimport { CancelNumberDto } from \"@/types/dto/numbering.dto\";\nimport { useState } from \"react\";\n\nconst formSchema = z.object({\n documentNumber: z.string().min(3, \"Document Number is required\"),\n reason: z.string().min(5, \"Reason must be at least 5 characters\"),\n});\n\ntype CancelNumberFormData = z.infer<typeof formSchema>;\n\nexport function CancelNumberForm() {\n const [loading, setLoading] = useState(false);\n\n const form = useForm<CancelNumberFormData>({\n resolver: zodResolver(formSchema) as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- zod 4 + @hookform/resolvers compat\n defaultValues: {\n documentNumber: \"\",\n reason: \"\",\n },\n });\n\n async function onSubmit(values: CancelNumberFormData) {\n setLoading(true);\n try {\n const dto: CancelNumberDto = values;\n await documentNumberingService.cancelNumber(dto);\n toast.success(\"Number cancelled successfully.\");\n form.reset();\n } catch (_error) {\n toast.error(\"Failed to cancel number. It may not exist or is already cancelled.\");\n } finally {\n setLoading(false);\n }\n }\n\n return (\n <Form {...form}>\n <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-4 border p-4 rounded-md\">\n <h3 className=\"text-lg font-medium\">Cancel Number</h3>\n <p className=\"text-sm text-gray-500\">Permanently cancel a number (e.g. if generated by mistake). It cannot be reused.</p>\n\n <FormField control={form.control} name=\"documentNumber\" render={({ field }) => (\n <FormItem>\n <FormLabel>Document Number</FormLabel>\n <FormControl>\n <Input placeholder=\"e.g. LCB3-COR-GGL-2025-0001\" {...field} />\n </FormControl>\n <FormMessage />\n </FormItem>\n )} />\n\n <FormField control={form.control} name=\"reason\" render={({ field }) => (\n <FormItem>\n <FormLabel>Reason</FormLabel>\n <FormControl>\n <Input placeholder=\"Reason for cancellation...\" {...field} />\n </FormControl>\n <FormMessage />\n </FormItem>\n )} />\n\n <Button type=\"submit\" variant=\"destructive\" disabled={loading}>\n {loading ? \"Cancelling...\" : \"Cancel Number\"}\n </Button>\n </form>\n </Form>\n );\n}\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\numbering\\manual-override-form.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'_error' is defined but never used.",
"line": 58,
"column": 14,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 58,
"endColumn": 20
}
],
"suppressedMessages": [
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 36,
"column": 42,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 36,
"endColumn": 45,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [1317, 1320], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [1317, 1320], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
],
"suppressions": [{ "kind": "directive", "justification": "zod 4 + @hookform/resolvers compat" }]
}
],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "\"use client\";\n\nimport { useForm } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport * as z from \"zod\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Form,\n FormControl,\n FormField,\n FormItem,\n FormLabel,\n FormMessage,\n FormDescription\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { toast } from \"sonner\";\nimport { documentNumberingService } from \"@/lib/services/document-numbering.service\";\nimport { ManualOverrideDto } from \"@/types/dto/numbering.dto\";\nimport { useState } from \"react\";\n\nconst formSchema = z.object({\n projectId: z.coerce.number().min(1, \"Project is required\"),\n originatorOrganizationId: z.coerce.number().min(1, \"Originator is required\"),\n recipientOrganizationId: z.coerce.number().min(1, \"Recipient is required\"),\n correspondenceTypeId: z.coerce.number().min(1, \"Type is required\"),\n newLastNumber: z.coerce.number().min(1, \"New number is required\"),\n reason: z.string().min(5, \"Reason must be at least 5 characters\"),\n resetScope: z.string().optional()\n});\n\nexport function ManualOverrideForm({ projectId = 1 }: { projectId?: number | string }) {\n const [loading, setLoading] = useState(false);\n\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema) as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- zod 4 + @hookform/resolvers compat\n defaultValues: {\n projectId: Number(projectId),\n originatorOrganizationId: 0,\n recipientOrganizationId: 0,\n correspondenceTypeId: 0,\n newLastNumber: 0,\n reason: \"\",\n resetScope: \"YEAR_2025\" // Example, should be dynamic or selected\n },\n });\n\n async function onSubmit(values: z.infer<typeof formSchema>) {\n setLoading(true);\n try {\n const dto: ManualOverrideDto = {\n ...values,\n resetScope: values.resetScope || \"YEAR_\" + new Date().getFullYear()\n };\n await documentNumberingService.manualOverride(dto);\n toast.success(\"Manual override applied successfully.\");\n form.reset();\n } catch (_error) {\n toast.error(\"Failed to apply override.\");\n } finally {\n setLoading(false);\n }\n }\n\n return (\n <Form {...form}>\n <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-4 border p-4 rounded-md mt-4\">\n <h3 className=\"text-lg font-medium\">Manual Override Sequence</h3>\n <p className=\"text-sm text-gray-500\">Careful: This updates the LAST generated number. Next number will receive +1.</p>\n\n <div className=\"grid grid-cols-2 gap-4\">\n {/* Allow simple text input for IDs for now, ideally Selects from Master Data */}\n <FormField control={form.control} name=\"projectId\" render={({ field }) => (\n <FormItem>\n <FormLabel>Project ID</FormLabel>\n <FormControl><Input type=\"number\" {...field} /></FormControl>\n <FormMessage />\n </FormItem>\n )} />\n <FormField control={form.control} name=\"correspondenceTypeId\" render={({ field }) => (\n <FormItem>\n <FormLabel>Type ID</FormLabel>\n <FormControl><Input type=\"number\" {...field} /></FormControl>\n <FormMessage />\n </FormItem>\n )} />\n <FormField control={form.control} name=\"originatorOrganizationId\" render={({ field }) => (\n <FormItem>\n <FormLabel>Originator Org ID</FormLabel>\n <FormControl><Input type=\"number\" {...field} /></FormControl>\n <FormMessage />\n </FormItem>\n )} />\n <FormField control={form.control} name=\"recipientOrganizationId\" render={({ field }) => (\n <FormItem>\n <FormLabel>Recipient Org ID</FormLabel>\n <FormControl><Input type=\"number\" {...field} /></FormControl>\n <FormMessage />\n </FormItem>\n )} />\n </div>\n\n <FormField control={form.control} name=\"newLastNumber\" render={({ field }) => (\n <FormItem>\n <FormLabel>Set Last Number To</FormLabel>\n <FormControl>\n <Input type=\"number\" {...field} />\n </FormControl>\n <FormDescription>\n If you set 99, the next auto-generated number will be 100.\n </FormDescription>\n <FormMessage />\n </FormItem>\n )} />\n\n <FormField control={form.control} name=\"reason\" render={({ field }) => (\n <FormItem>\n <FormLabel>Reason</FormLabel>\n <FormControl>\n <Input placeholder=\"Why are you overriding?\" {...field} />\n </FormControl>\n <FormMessage />\n </FormItem>\n )} />\n\n <Button type=\"submit\" disabled={loading}>\n {loading ? \"Applying...\" : \"Apply Override\"}\n </Button>\n </form>\n </Form>\n );\n}\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\numbering\\metrics-dashboard.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'error' is defined but never used.",
"line": 18,
"column": 16,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 18,
"endColumn": 21
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { Card, CardContent, CardHeader, CardTitle, _CardDescription } from \"@/components/ui/card\";\nimport { Progress } from \"@/components/ui/progress\";\nimport { documentNumberingService } from \"@/lib/services/document-numbering.service\";\nimport { NumberingMetrics } from \"@/types/dto/numbering.dto\";\n\nexport function MetricsDashboard() {\n const [metrics, setMetrics] = useState<Partial<NumberingMetrics>>({});\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n async function fetchMetrics() {\n try {\n const data = await documentNumberingService.getMetrics();\n setMetrics(data);\n } catch (error) {\n // Failed to fetch metrics - handled by loading state\n } finally {\n setLoading(false);\n }\n }\n fetchMetrics();\n const interval = setInterval(fetchMetrics, 30000); // Poll every 30s\n return () => clearInterval(interval);\n }, []);\n\n if (loading) return <div>Loading metrics...</div>;\n if (!metrics) return <div>No metrics available.</div>;\n\n // Mock data mapping if real data is missing from backend stub\n const utilization = metrics.audit ? 45 : 0; // Placeholder until backend returns specific metric\n const generationRate = 120; // Placeholder\n const lockWaitP95 = 0.05; // Placeholder\n\n return (\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Generation Rate</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold\">{generationRate} /Hr</div>\n <p className=\"text-xs text-muted-foreground\">+20.1% from last hour</p>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Sequence Utilization</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold\">{utilization}%</div>\n <Progress value={utilization} className=\"mt-2\" />\n <p className=\"text-xs text-muted-foreground mt-1\">Average capacity used</p>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Lock Wait Time (P95)</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold\">{lockWaitP95}s</div>\n <p className=\"text-xs text-muted-foreground\">Redis distributed lock latency</p>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Recent Errors</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold\">{metrics.errors?.length || 0}</div>\n <p className=\"text-xs text-muted-foreground\">In the last 24 hours</p>\n </CardContent>\n </Card>\n </div>\n );\n}\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\numbering\\sequence-viewer.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\numbering\\template-editor.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\numbering\\template-tester.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\numbering\\void-replace-form.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'_error' is defined but never used.",
"line": 54,
"column": 14,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 54,
"endColumn": 20
}
],
"suppressedMessages": [
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 36,
"column": 42,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 36,
"endColumn": 45,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [1134, 1137], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [1134, 1137], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
],
"suppressions": [{ "kind": "directive", "justification": "zod 4 + @hookform/resolvers compat" }]
}
],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "\"use client\";\n\nimport { useForm } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport * as z from \"zod\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Form,\n FormControl,\n FormField,\n FormItem,\n FormLabel,\n FormMessage,\n FormDescription\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { toast } from \"sonner\";\nimport { documentNumberingService } from \"@/lib/services/document-numbering.service\";\nimport { VoidReplaceDto } from \"@/types/dto/numbering.dto\";\nimport { useState } from \"react\";\n\nconst formSchema = z.object({\n documentNumber: z.string().min(3, \"Document Number is required\"),\n reason: z.string().min(5, \"Reason must be at least 5 characters\"),\n replace: z.boolean(),\n projectId: z.number()\n});\n\ntype VoidReplaceFormData = z.infer<typeof formSchema>;\n\nexport function VoidReplaceForm({ projectId = 1 }: { projectId?: number | string }) {\n const [loading, setLoading] = useState(false);\n\n const form = useForm<VoidReplaceFormData>({\n resolver: zodResolver(formSchema) as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- zod 4 + @hookform/resolvers compat\n defaultValues: {\n documentNumber: \"\",\n reason: \"\",\n replace: false,\n projectId: Number(projectId)\n },\n });\n\n async function onSubmit(values: z.infer<typeof formSchema>) {\n setLoading(true);\n try {\n const dto: VoidReplaceDto = {\n ...values,\n };\n await documentNumberingService.voidAndReplace(dto);\n toast.success(\"Number voided successfully. \" + (values.replace ? \"Replacement generated.\" : \"\"));\n form.reset();\n } catch (_error) {\n toast.error(\"Failed to void number. Check if it exists.\");\n } finally {\n setLoading(false);\n }\n }\n\n return (\n <Form {...form}>\n <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-4 border p-4 rounded-md\">\n <h3 className=\"text-lg font-medium\">Void & Replace Number</h3>\n <p className=\"text-sm text-gray-500\">Void a generated number. Useful for skipped numbers or errors.</p>\n\n <FormField control={form.control} name=\"documentNumber\" render={({ field }) => (\n <FormItem>\n <FormLabel>Document Number</FormLabel>\n <FormControl>\n <Input placeholder=\"e.g. LCB3-COR-GGL-2025-0001\" {...field} />\n </FormControl>\n <FormMessage />\n </FormItem>\n )} />\n\n <FormField control={form.control} name=\"reason\" render={({ field }) => (\n <FormItem>\n <FormLabel>Reason</FormLabel>\n <FormControl>\n <Input placeholder=\"Reason for voiding...\" {...field} />\n </FormControl>\n <FormMessage />\n </FormItem>\n )} />\n\n <FormField control={form.control} name=\"replace\" render={({ field }) => (\n <FormItem className=\"flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4\">\n <FormControl>\n <Checkbox\n checked={field.value}\n onCheckedChange={field.onChange}\n />\n </FormControl>\n <div className=\"space-y-1 leading-none\">\n <FormLabel>\n Generate Replacement?\n </FormLabel>\n <FormDescription>\n If checked, a new number will be reserved immediately.\n </FormDescription>\n </div>\n </FormItem>\n )} />\n\n <Button type=\"submit\" variant=\"destructive\" disabled={loading}>\n {loading ? \"Processing...\" : \"Void Number\"}\n </Button>\n </form>\n </Form>\n );\n}\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\rfas\\detail.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\rfas\\form.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'_err' is defined but never used.",
"line": 285,
"column": 17,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 285,
"endColumn": 21
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "\"use client\";\r\n\r\nimport { useForm, type SubmitErrorHandler } from \"react-hook-form\";\r\nimport { zodResolver } from \"@hookform/resolvers/zod\";\r\nimport { z } from \"zod\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Checkbox } from \"@/components/ui/checkbox\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Card } from \"@/components/ui/card\";\r\nimport { Loader2 } from \"lucide-react\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport { useRouter } from \"next/navigation\";\r\nimport { useCreateRFA } from \"@/hooks/use-rfa\";\r\nimport { useDrawings } from \"@/hooks/use-drawing\";\r\nimport { useDisciplines, useContracts, useOrganizations } from \"@/hooks/use-master-data\";\r\nimport { useCorrespondenceTypes, useRfaTypes } from \"@/hooks/use-reference-data\";\r\nimport { useProjects } from \"@/hooks/use-projects\";\r\nimport { CreateRfaDto } from \"@/types/dto/rfa/rfa.dto\";\r\nimport { useState, useEffect, type FormEvent } from \"react\";\r\nimport { correspondenceService } from \"@/lib/services/correspondence.service\";\r\n\r\nconst rfaSchema = z.object({\r\n projectId: z.string().min(1, \"Project is required\"), // ADR-019: UUID\r\n contractId: z.string().min(1, \"Contract is required\"),\r\n disciplineId: z.number().min(1, \"Discipline is required\"),\r\n rfaTypeId: z.number().min(1, \"Type is required\"),\r\n subject: z.string().min(5, \"Subject must be at least 5 characters\"),\r\n description: z.string().optional(),\r\n body: z.string().optional(),\r\n remarks: z.string().optional(),\r\n toOrganizationId: z.string().min(1, \"Please select To Organization\"),\r\n dueDate: z.string().optional(),\r\n shopDrawingRevisionIds: z.array(z.string()).optional(),\r\n asBuiltDrawingRevisionIds: z.array(z.string()).optional(),\r\n});\r\n\r\ntype RFAFormData = z.infer<typeof rfaSchema>;\r\n\r\ntype ProjectOption = {\r\n uuid?: string;\r\n id?: number;\r\n projectName?: string;\r\n projectCode?: string;\r\n};\r\n\r\ntype ContractOption = {\r\n uuid?: string;\r\n id?: number;\r\n contractName?: string;\r\n name?: string;\r\n contractCode?: string;\r\n};\r\n\r\ntype DisciplineOption = {\r\n id: number;\r\n disciplineCode: string;\r\n codeNameEn?: string;\r\n codeNameTh?: string;\r\n};\r\n\r\ntype RfaTypeOption = {\r\n id: number;\r\n typeCode?: string;\r\n typeName?: string;\r\n typeNameEn?: string;\r\n typeNameTh?: string;\r\n};\r\n\r\ntype CorrespondenceTypeOption = {\r\n id: number;\r\n typeCode?: string;\r\n typeName?: string;\r\n};\r\n\r\ntype OrganizationOption = {\r\n uuid?: string;\r\n id?: number;\r\n organizationCode?: string;\r\n organizationName?: string;\r\n};\r\n\r\ntype SelectableDrawingOption = {\r\n uuid?: string;\r\n drawingNumber?: string;\r\n title?: string;\r\n legacyDrawingNumber?: string;\r\n currentRevisionUuid?: string;\r\n currentRevision?: {\r\n uuid?: string;\r\n revisionLabel?: string;\r\n revisionNumber?: number | string;\r\n title?: string;\r\n legacyDrawingNumber?: string;\r\n };\r\n};\r\n\r\nconst extractArrayData = <T,>(value: unknown): T[] => {\r\n let current: unknown = value;\r\n\r\n for (let i = 0; i < 5; i += 1) {\r\n if (Array.isArray(current)) {\r\n return current as T[];\r\n }\r\n\r\n if (!current || typeof current !== \"object\" || !(\"data\" in current)) {\r\n return [];\r\n }\r\n\r\n current = (current as { data?: unknown }).data;\r\n }\r\n\r\n return Array.isArray(current) ? (current as T[]) : [];\r\n};\r\n\r\nconst dedupeByKey = <T,>(items: T[], getKey: (item: T) => string | number | undefined): T[] => {\r\n const seen = new Set<string | number>();\r\n\r\n return items.filter((item) => {\r\n const key = getKey(item);\r\n\r\n if (key === undefined || key === \"\" || seen.has(key)) {\r\n return false;\r\n }\r\n\r\n seen.add(key);\r\n return true;\r\n });\r\n};\r\n\r\nconst getOptionValue = (value?: string | number): string | undefined => {\r\n if (value === undefined || value === null || value === \"\") {\r\n return undefined;\r\n }\r\n\r\n return String(value);\r\n};\r\n\r\nexport function RFAForm() {\r\n const router = useRouter();\r\n const createMutation = useCreateRFA();\r\n\r\n const { data: projectsData, isLoading: isLoadingProjects } = useProjects();\r\n const projects = dedupeByKey(\r\n extractArrayData<ProjectOption>(projectsData),\r\n (project) => project.uuid ?? project.id\r\n );\r\n const { data: organizationsData, isLoading: isLoadingOrganizations } = useOrganizations({ isActive: true });\r\n const organizations = dedupeByKey(\r\n extractArrayData<OrganizationOption>(organizationsData),\r\n (organization) => organization.uuid ?? organization.id\r\n );\r\n const { data: correspondenceTypesData } = useCorrespondenceTypes();\r\n const correspondenceTypes = extractArrayData<CorrespondenceTypeOption>(correspondenceTypesData);\r\n const rfaCorrespondenceType = correspondenceTypes.find(\r\n (type) => type.typeCode?.toUpperCase() === \"RFA\"\r\n );\r\n\r\n const {\r\n register,\r\n handleSubmit,\r\n setValue,\r\n setError,\r\n clearErrors,\r\n watch,\r\n formState: { errors },\r\n } = useForm<RFAFormData>({\r\n resolver: zodResolver(rfaSchema),\r\n defaultValues: {\r\n projectId: \"\",\r\n contractId: \"\",\r\n disciplineId: 0,\r\n rfaTypeId: 0,\r\n subject: \"\",\r\n description: \"\",\r\n body: \"\",\r\n remarks: \"\",\r\n toOrganizationId: \"\",\r\n dueDate: \"\",\r\n shopDrawingRevisionIds: [],\r\n asBuiltDrawingRevisionIds: [],\r\n },\r\n });\r\n\r\n const selectedProjectId = watch(\"projectId\");\r\n const { data: contractsData, isLoading: isLoadingContracts } = useContracts(selectedProjectId);\r\n const contracts = dedupeByKey(\r\n extractArrayData<ContractOption>(contractsData),\r\n (contract) => contract.uuid ?? contract.id\r\n );\r\n\r\n const selectedContractId = watch(\"contractId\");\r\n const { data: disciplinesData, isLoading: isLoadingDisciplines } = useDisciplines(selectedContractId);\r\n const disciplines = dedupeByKey(extractArrayData<DisciplineOption>(disciplinesData), (discipline) => discipline.id);\r\n const { data: rfaTypesData, isLoading: isLoadingRfaTypes } = useRfaTypes(selectedContractId);\r\n const rfaTypes = dedupeByKey(extractArrayData<RfaTypeOption>(rfaTypesData), (rfaType) => rfaType.id);\r\n const [shopDrawingSearch, setShopDrawingSearch] = useState(\"\");\r\n const [shopDrawingPage, setShopDrawingPage] = useState(1);\r\n const { data: shopDrawingsData, isLoading: isLoadingShopDrawings } = useDrawings(\"SHOP\", {\r\n projectUuid: selectedProjectId || \"\",\r\n search: shopDrawingSearch,\r\n page: shopDrawingPage,\r\n limit: 10,\r\n });\r\n const shopDrawings = dedupeByKey(\r\n extractArrayData<SelectableDrawingOption>(shopDrawingsData),\r\n (drawing) => drawing.currentRevisionUuid ?? drawing.currentRevision?.uuid ?? drawing.uuid\r\n );\r\n\r\n const [asBuiltDrawingSearch, setAsBuiltDrawingSearch] = useState(\"\");\r\n const [asBuiltDrawingPage, setAsBuiltDrawingPage] = useState(1);\r\n const { data: asBuiltDrawingsData, isLoading: isLoadingAsBuiltDrawings } = useDrawings(\"AS_BUILT\", {\r\n projectUuid: selectedProjectId || \"\",\r\n search: asBuiltDrawingSearch,\r\n page: asBuiltDrawingPage,\r\n limit: 10,\r\n });\r\n const asBuiltDrawings = dedupeByKey(\r\n extractArrayData<SelectableDrawingOption>(asBuiltDrawingsData),\r\n (drawing) => drawing.currentRevisionUuid ?? drawing.currentRevision?.uuid ?? drawing.uuid\r\n );\r\n const selectedDisciplineId = watch(\"disciplineId\");\r\n\r\n const rfaTypeId = watch(\"rfaTypeId\");\r\n const disciplineId = watch(\"disciplineId\");\r\n const toOrganizationId = watch(\"toOrganizationId\");\r\n const selectedShopDrawingRevisionIds = watch(\"shopDrawingRevisionIds\") ?? [];\r\n const selectedAsBuiltDrawingRevisionIds = watch(\"asBuiltDrawingRevisionIds\") ?? [];\r\n const selectedRfaType = rfaTypes.find((rfaType) => rfaType.id === rfaTypeId);\r\n const selectedRfaTypeCode = selectedRfaType?.typeCode?.toUpperCase();\r\n const requiresShopDrawings = selectedRfaTypeCode === \"DDW\" || selectedRfaTypeCode === \"SDW\";\r\n const requiresAsBuiltDrawings = selectedRfaTypeCode === \"ADW\";\r\n\r\n useEffect(() => {\r\n // Reset page and search when project changes\r\n setShopDrawingPage(1);\r\n setShopDrawingSearch(\"\");\r\n setAsBuiltDrawingPage(1);\r\n setAsBuiltDrawingSearch(\"\");\r\n\r\n if (requiresShopDrawings) {\r\n setValue(\"asBuiltDrawingRevisionIds\", []);\r\n clearErrors(\"asBuiltDrawingRevisionIds\");\r\n return;\r\n }\r\n\r\n if (requiresAsBuiltDrawings) {\r\n setValue(\"shopDrawingRevisionIds\", []);\r\n clearErrors(\"shopDrawingRevisionIds\");\r\n return;\r\n }\r\n\r\n setValue(\"shopDrawingRevisionIds\", []);\r\n setValue(\"asBuiltDrawingRevisionIds\", []);\r\n clearErrors(\"shopDrawingRevisionIds\");\r\n clearErrors(\"asBuiltDrawingRevisionIds\");\r\n }, [requiresShopDrawings, requiresAsBuiltDrawings, selectedProjectId, setValue, clearErrors]);\r\n\r\n // -- Preview Logic --\r\n const [preview, setPreview] = useState<{ number: string; isDefaultTemplate: boolean } | null>(null);\r\n\r\n useEffect(() => {\r\n if (!selectedProjectId || !rfaCorrespondenceType?.id || !rfaTypeId || !disciplineId || !toOrganizationId) {\r\n setPreview(null);\r\n return;\r\n }\r\n\r\n const fetchPreview = async () => {\r\n try {\r\n const res = await correspondenceService.previewNumber({\r\n projectId: selectedProjectId,\r\n typeId: rfaCorrespondenceType.id,\r\n disciplineId,\r\n recipients: [{ organizationId: toOrganizationId, type: 'TO' }],\r\n subject: watch(\"subject\") || \"Preview Subject\"\r\n });\r\n setPreview(res);\r\n } catch (_err) {\r\n setPreview(null);\r\n }\r\n };\r\n\r\n const timer = setTimeout(fetchPreview, 500);\r\n return () => clearTimeout(timer);\r\n }, [rfaTypeId, disciplineId, toOrganizationId, selectedProjectId, rfaCorrespondenceType?.id, watch]);\r\n\r\n const onSubmit = (data: RFAFormData) => {\r\n if (requiresShopDrawings && data.shopDrawingRevisionIds?.length === 0) {\r\n setError(\"shopDrawingRevisionIds\", {\r\n type: \"manual\",\r\n message: \"Please select at least one Shop Drawing Revision\",\r\n });\r\n return;\r\n }\r\n\r\n if (requiresAsBuiltDrawings && data.asBuiltDrawingRevisionIds?.length === 0) {\r\n setError(\"asBuiltDrawingRevisionIds\", {\r\n type: \"manual\",\r\n message: \"Please select at least one As-Built Drawing Revision\",\r\n });\r\n return;\r\n }\r\n\r\n clearErrors(\"shopDrawingRevisionIds\");\r\n clearErrors(\"asBuiltDrawingRevisionIds\");\r\n\r\n const payload: CreateRfaDto = {\r\n ...data,\r\n shopDrawingRevisionIds: requiresShopDrawings ? data.shopDrawingRevisionIds : undefined,\r\n asBuiltDrawingRevisionIds: requiresAsBuiltDrawings ? data.asBuiltDrawingRevisionIds : undefined,\r\n };\r\n createMutation.mutate(payload, {\r\n onSuccess: () => {\r\n router.push(\"/rfas\");\r\n },\r\n });\r\n };\r\n\r\n const onInvalidSubmit: SubmitErrorHandler<RFAFormData> = () => undefined;\r\n const submitForm = handleSubmit(onSubmit, onInvalidSubmit);\r\n const handleFormSubmit = (event: FormEvent<HTMLFormElement>) => {\r\n void submitForm(event).catch(() => undefined);\r\n };\r\n\r\n return (\r\n <form onSubmit={handleFormSubmit} className=\"max-w-4xl space-y-6\">\r\n {preview && (\r\n <Card className=\"p-4 bg-muted border-l-4 border-l-primary\">\r\n <p className=\"text-sm text-muted-foreground mb-1\">Document Number Preview</p>\r\n <div className=\"flex items-center gap-3\">\r\n <span className=\"text-xl font-bold font-mono text-primary tracking-wide\">{preview.number}</span>\r\n {preview.isDefaultTemplate && (\r\n <span className=\"text-[10px] uppercase font-semibold px-2 py-0.5 rounded-full bg-yellow-100 text-yellow-800 border border-yellow-200\">\r\n Default Template\r\n </span>\r\n )}\r\n </div>\r\n </Card>\r\n )}\r\n\r\n <Card className=\"p-6\">\r\n <h3 className=\"text-lg font-semibold mb-4\">RFA Information</h3>\r\n\r\n <div className=\"space-y-4\">\r\n <div>\r\n <Label htmlFor=\"subject\">Subject *</Label>\r\n <Input id=\"subject\" {...register(\"subject\")} placeholder=\"Enter subject\" />\r\n {errors.subject && (\r\n <p className=\"text-sm text-destructive mt-1\">\r\n {errors.subject.message}\r\n </p>\r\n )}\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"body\">Body (Content)</Label>\r\n <Textarea id=\"body\" {...register(\"body\")} rows={4} placeholder=\"Enter content...\" />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"remarks\">Remarks</Label>\r\n <Input id=\"remarks\" {...register(\"remarks\")} placeholder=\"Optional remarks\" />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"description\">Description</Label>\r\n <Input id=\"description\" {...register(\"description\")} placeholder=\"Enter key description\" />\r\n </div>\r\n\r\n <div>\r\n <Label>Project *</Label>\r\n <Select\r\n value={selectedProjectId || undefined}\r\n onValueChange={(val) => {\r\n setValue(\"projectId\", val);\r\n setValue(\"contractId\", \"\");\r\n setValue(\"disciplineId\", 0);\r\n setValue(\"rfaTypeId\", 0);\r\n setValue(\"shopDrawingRevisionIds\", []);\r\n setValue(\"asBuiltDrawingRevisionIds\", []);\r\n }}\r\n disabled={isLoadingProjects}\r\n >\r\n <SelectTrigger>\r\n <SelectValue placeholder={isLoadingProjects ? \"Loading...\" : \"Select Project\"} />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {projects.map((p) => {\r\n const projectValue = getOptionValue(p.uuid ?? p.id);\r\n\r\n if (!projectValue) {\r\n return null;\r\n }\r\n\r\n return (\r\n <SelectItem key={projectValue} value={projectValue}>\r\n {p.projectName || p.projectCode}\r\n </SelectItem>\r\n );\r\n })}\r\n </SelectContent>\r\n </Select>\r\n {errors.projectId && (\r\n <p className=\"text-sm text-destructive mt-1\">{errors.projectId.message}</p>\r\n )}\r\n </div>\r\n\r\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\r\n <div>\r\n <Label>Contract *</Label>\r\n <Select\r\n value={selectedContractId || undefined}\r\n onValueChange={(val) => {\r\n setValue(\"contractId\", val);\r\n setValue(\"disciplineId\", 0);\r\n setValue(\"rfaTypeId\", 0);\r\n setValue(\"shopDrawingRevisionIds\", []);\r\n setValue(\"asBuiltDrawingRevisionIds\", []);\r\n }}\r\n disabled={!selectedProjectId || isLoadingContracts}\r\n >\r\n <SelectTrigger>\r\n <SelectValue placeholder={isLoadingContracts ? \"Loading...\" : \"Select Contract\"} />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {contracts.map((c) => {\r\n const contractValue = getOptionValue(c.uuid ?? c.id);\r\n\r\n if (!contractValue) {\r\n return null;\r\n }\r\n\r\n return (\r\n <SelectItem key={contractValue} value={contractValue}>\r\n {c.contractName || c.name || c.contractCode}\r\n </SelectItem>\r\n );\r\n })}\r\n </SelectContent>\r\n </Select>\r\n {errors.contractId && (\r\n <p className=\"text-sm text-destructive mt-1\">{errors.contractId.message}</p>\r\n )}\r\n </div>\r\n\r\n <div>\r\n <Label>Discipline *</Label>\r\n <Select\r\n value={selectedDisciplineId > 0 ? String(selectedDisciplineId) : undefined}\r\n onValueChange={(val) => setValue(\"disciplineId\", Number(val))}\r\n disabled={!selectedContractId || isLoadingDisciplines}\r\n >\r\n <SelectTrigger>\r\n <SelectValue placeholder={isLoadingDisciplines ? \"Loading...\" : \"Select Discipline\"} />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {disciplines.map((d) => (\r\n <SelectItem key={d.id} value={String(d.id)}>\r\n {`${d.codeNameEn || d.codeNameTh || d.disciplineCode} (${d.disciplineCode})`}\r\n </SelectItem>\r\n ))}\r\n {!isLoadingDisciplines && disciplines.length === 0 && (\r\n <SelectItem value=\"0\" disabled>No disciplines found</SelectItem>\r\n )}\r\n </SelectContent>\r\n </Select>\r\n {errors.disciplineId && (\r\n <p className=\"text-sm text-destructive mt-1\">{errors.disciplineId.message}</p>\r\n )}\r\n </div>\r\n </div>\r\n\r\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\r\n <div>\r\n <Label>RFA Type *</Label>\r\n <Select\r\n value={rfaTypeId > 0 ? String(rfaTypeId) : undefined}\r\n onValueChange={(val) => {\r\n setValue(\"rfaTypeId\", Number(val));\r\n setValue(\"shopDrawingRevisionIds\", []);\r\n setValue(\"asBuiltDrawingRevisionIds\", []);\r\n }}\r\n disabled={!selectedContractId || isLoadingRfaTypes}\r\n >\r\n <SelectTrigger>\r\n <SelectValue placeholder={isLoadingRfaTypes ? \"Loading...\" : \"Select RFA Type\"} />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {rfaTypes.map((rfaType) => (\r\n <SelectItem key={rfaType.id} value={String(rfaType.id)}>\r\n {`${rfaType.typeCode || \"RFA\"} - ${rfaType.typeName || rfaType.typeNameEn || rfaType.typeNameTh || \"Unnamed Type\"}`}\r\n </SelectItem>\r\n ))}\r\n </SelectContent>\r\n </Select>\r\n {errors.rfaTypeId && (\r\n <p className=\"text-sm text-destructive mt-1\">{errors.rfaTypeId.message}</p>\r\n )}\r\n </div>\r\n\r\n <div>\r\n <Label>To Organization *</Label>\r\n <Select\r\n value={toOrganizationId || undefined}\r\n onValueChange={(val) => setValue(\"toOrganizationId\", val)}\r\n disabled={isLoadingOrganizations}\r\n >\r\n <SelectTrigger>\r\n <SelectValue placeholder={isLoadingOrganizations ? \"Loading...\" : \"Select To Organization\"} />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {organizations.map((organization) => {\r\n const organizationValue = getOptionValue(organization.uuid ?? organization.id);\r\n\r\n if (!organizationValue) {\r\n return null;\r\n }\r\n\r\n return (\r\n <SelectItem key={organizationValue} value={organizationValue}>\r\n {`${organization.organizationCode || \"ORG\"} - ${organization.organizationName || \"Unnamed Organization\"}`}\r\n </SelectItem>\r\n );\r\n })}\r\n </SelectContent>\r\n </Select>\r\n {errors.toOrganizationId && (\r\n <p className=\"text-sm text-destructive mt-1\">{errors.toOrganizationId.message}</p>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n </Card>\r\n\r\n {(requiresShopDrawings || requiresAsBuiltDrawings) && (\r\n <Card className=\"p-6\">\r\n <h3 className=\"text-lg font-semibold mb-4\">New Item</h3>\r\n <div className=\"space-y-3\">\r\n <p className=\"text-sm text-muted-foreground\">\r\n {requiresShopDrawings\r\n ? \"RFA Type นี้ต้องอ้างอิง Shop Drawing Revision อย่างน้อย 1 รายการ\"\r\n : \"RFA Type นี้ต้องอ้างอิง As-Built Drawing Revision อย่างน้อย 1 รายการ\"}\r\n </p>\r\n\r\n {requiresShopDrawings && (\r\n <div className=\"space-y-3\">\r\n <div className=\"flex items-center gap-2 mb-2\">\r\n <Input\r\n placeholder=\"ค้นหาตาม Drawing Number...\"\r\n value={shopDrawingSearch}\r\n onChange={(e) => {\r\n setShopDrawingSearch(e.target.value);\r\n setShopDrawingPage(1);\r\n }}\r\n className=\"max-w-xs\"\r\n />\r\n </div>\r\n\r\n {isLoadingShopDrawings && (\r\n <p className=\"text-sm text-muted-foreground\">Loading Shop Drawings...</p>\r\n )}\r\n {!isLoadingShopDrawings && shopDrawings.length === 0 && (\r\n <p className=\"text-sm text-muted-foreground\">No Shop Drawings found for the selected project.</p>\r\n )}\r\n <div className=\"grid grid-cols-1 gap-3\">\r\n {shopDrawings.map((drawing) => {\r\n const revisionUuid = drawing.currentRevisionUuid ?? drawing.currentRevision?.uuid;\r\n\r\n if (!revisionUuid) {\r\n return null;\r\n }\r\n\r\n return (\r\n <label\r\n key={revisionUuid}\r\n className=\"flex items-start gap-3 rounded-lg border p-3 cursor-pointer hover:bg-muted/50 transition-colors\"\r\n >\r\n <Checkbox\r\n checked={selectedShopDrawingRevisionIds.includes(revisionUuid)}\r\n onCheckedChange={(checked) => {\r\n const nextValues = checked === true\r\n ? [...selectedShopDrawingRevisionIds, revisionUuid]\r\n : selectedShopDrawingRevisionIds.filter((value) => value !== revisionUuid);\r\n setValue(\"shopDrawingRevisionIds\", nextValues, { shouldDirty: true, shouldValidate: true });\r\n clearErrors(\"shopDrawingRevisionIds\");\r\n }}\r\n />\r\n <div className=\"space-y-1\">\r\n <p className=\"font-medium\">{drawing.drawingNumber || \"Unnamed Shop Drawing\"}</p>\r\n <p className=\"text-sm text-muted-foreground\">{drawing.currentRevision?.title || drawing.title || \"Untitled Revision\"}</p>\r\n <p className=\"text-xs text-muted-foreground\">\r\n Revision {drawing.currentRevision?.revisionLabel || drawing.currentRevision?.revisionNumber || \"-\"}\r\n {drawing.currentRevision?.legacyDrawingNumber ? ` • Legacy ${drawing.currentRevision.legacyDrawingNumber}` : \"\"}\r\n </p>\r\n </div>\r\n </label>\r\n );\r\n })}\r\n </div>\r\n\r\n {shopDrawingsData?.meta && shopDrawingsData.meta.totalPages > 1 && (\r\n <div className=\"flex items-center justify-between mt-4\">\r\n <p className=\"text-xs text-muted-foreground\">\r\n Page {shopDrawingPage} of {shopDrawingsData.meta.totalPages}\r\n </p>\r\n <div className=\"flex gap-2\">\r\n <Button\r\n type=\"button\"\r\n variant=\"outline\"\r\n size=\"sm\"\r\n disabled={shopDrawingPage === 1 || isLoadingShopDrawings}\r\n onClick={() => setShopDrawingPage((p) => Math.max(1, p - 1))}\r\n >\r\n Previous\r\n </Button>\r\n <Button\r\n type=\"button\"\r\n variant=\"outline\"\r\n size=\"sm\"\r\n disabled={shopDrawingPage >= shopDrawingsData.meta.totalPages || isLoadingShopDrawings}\r\n onClick={() => setShopDrawingPage((p) => p + 1)}\r\n >\r\n Next\r\n </Button>\r\n </div>\r\n </div>\r\n )}\r\n\r\n {errors.shopDrawingRevisionIds && (\r\n <p className=\"text-sm text-destructive mt-2\">{errors.shopDrawingRevisionIds.message}</p>\r\n )}\r\n </div>\r\n )}\r\n\r\n {requiresAsBuiltDrawings && (\r\n <div className=\"space-y-3\">\r\n <div className=\"flex items-center gap-2 mb-2\">\r\n <Input\r\n placeholder=\"ค้นหาตาม Drawing Number...\"\r\n value={asBuiltDrawingSearch}\r\n onChange={(e) => {\r\n setAsBuiltDrawingSearch(e.target.value);\r\n setAsBuiltDrawingPage(1);\r\n }}\r\n className=\"max-w-xs\"\r\n />\r\n </div>\r\n\r\n {isLoadingAsBuiltDrawings && (\r\n <p className=\"text-sm text-muted-foreground\">Loading As-Built Drawings...</p>\r\n )}\r\n {!isLoadingAsBuiltDrawings && asBuiltDrawings.length === 0 && (\r\n <p className=\"text-sm text-muted-foreground\">No As-Built Drawings found for the selected project.</p>\r\n )}\r\n <div className=\"grid grid-cols-1 gap-3\">\r\n {asBuiltDrawings.map((drawing) => {\r\n const revisionUuid = drawing.currentRevisionUuid ?? drawing.currentRevision?.uuid;\r\n\r\n if (!revisionUuid) {\r\n return null;\r\n }\r\n\r\n return (\r\n <label\r\n key={revisionUuid}\r\n className=\"flex items-start gap-3 rounded-lg border p-3 cursor-pointer hover:bg-muted/50 transition-colors\"\r\n >\r\n <Checkbox\r\n checked={selectedAsBuiltDrawingRevisionIds.includes(revisionUuid)}\r\n onCheckedChange={(checked) => {\r\n const nextValues = checked === true\r\n ? [...selectedAsBuiltDrawingRevisionIds, revisionUuid]\r\n : selectedAsBuiltDrawingRevisionIds.filter((value) => value !== revisionUuid);\r\n setValue(\"asBuiltDrawingRevisionIds\", nextValues, { shouldDirty: true, shouldValidate: true });\r\n clearErrors(\"asBuiltDrawingRevisionIds\");\r\n }}\r\n />\r\n <div className=\"space-y-1\">\r\n <p className=\"font-medium\">{drawing.drawingNumber || \"Unnamed As-Built Drawing\"}</p>\r\n <p className=\"text-sm text-muted-foreground\">{drawing.currentRevision?.title || drawing.title || \"Untitled Revision\"}</p>\r\n <p className=\"text-xs text-muted-foreground\">\r\n Revision {drawing.currentRevision?.revisionLabel || drawing.currentRevision?.revisionNumber || \"-\"}\r\n {drawing.currentRevision?.legacyDrawingNumber ? ` • Legacy ${drawing.currentRevision.legacyDrawingNumber}` : \"\"}\r\n </p>\r\n </div>\r\n </label>\r\n );\r\n })}\r\n </div>\r\n\r\n {asBuiltDrawingsData?.meta && asBuiltDrawingsData.meta.totalPages > 1 && (\r\n <div className=\"flex items-center justify-between mt-4\">\r\n <p className=\"text-xs text-muted-foreground\">\r\n Page {asBuiltDrawingPage} of {asBuiltDrawingsData.meta.totalPages}\r\n </p>\r\n <div className=\"flex gap-2\">\r\n <Button\r\n type=\"button\"\r\n variant=\"outline\"\r\n size=\"sm\"\r\n disabled={asBuiltDrawingPage === 1 || isLoadingAsBuiltDrawings}\r\n onClick={() => setAsBuiltDrawingPage((p) => Math.max(1, p - 1))}\r\n >\r\n Previous\r\n </Button>\r\n <Button\r\n type=\"button\"\r\n variant=\"outline\"\r\n size=\"sm\"\r\n disabled={asBuiltDrawingPage >= asBuiltDrawingsData.meta.totalPages || isLoadingAsBuiltDrawings}\r\n onClick={() => setAsBuiltDrawingPage((p) => p + 1)}\r\n >\r\n Next\r\n </Button>\r\n </div>\r\n </div>\r\n )}\r\n\r\n {errors.asBuiltDrawingRevisionIds && (\r\n <p className=\"text-sm text-destructive mt-2\">{errors.asBuiltDrawingRevisionIds.message}</p>\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n </Card>\r\n )}\r\n\r\n <div className=\"flex justify-end gap-3\">\r\n <Button type=\"button\" variant=\"outline\" onClick={() => router.back()}>\r\n Cancel\r\n </Button>\r\n <Button type=\"submit\" disabled={createMutation.isPending}>\r\n {createMutation.isPending && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\r\n Create RFA\r\n </Button>\r\n </div>\r\n </form>\r\n );\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\rfas\\list.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\search\\filters.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\search\\results.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\transmittal\\transmittal-form.tsx",
"messages": [],
"suppressedMessages": [
{
"ruleId": "@typescript-eslint/no-explicit-any",
"severity": 2,
"message": "Unexpected any. Specify a different type.",
"line": 80,
"column": 42,
"nodeType": "TSAnyKeyword",
"messageId": "unexpectedAny",
"endLine": 80,
"endColumn": 45,
"suggestions": [
{
"messageId": "suggestUnknown",
"fix": { "range": [2584, 2587], "text": "unknown" },
"desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
},
{
"messageId": "suggestNever",
"fix": { "range": [2584, 2587], "text": "never" },
"desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
}
],
"suppressions": [{ "kind": "directive", "justification": "zod 4 + @hookform/resolvers compat" }]
}
],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\transmittal\\transmittal-list.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\__tests__\\button.test.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\alert-dialog.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\alert.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\avatar.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\badge.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\button.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\calendar.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\card.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\checkbox.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\command.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\dialog.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\dropdown-menu.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'Check' is defined but never used. Allowed unused vars must match /^_/u.",
"line": 6,
"column": 10,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 6,
"endColumn": 15,
"suggestions": [
{
"messageId": "removeUnusedVar",
"data": { "varName": "Check" },
"fix": { "range": [171, 177], "text": "" },
"desc": "Remove unused variable \"Check\"."
}
]
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "// File: components/ui/dropdown-menu.tsx\r\n\"use client\"\r\n\r\nimport * as React from \"react\"\r\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\r\nimport { Check, ChevronRight, _Circle } from \"lucide-react\"\r\nimport { cn } from \"@/lib/utils\"\r\n\r\nconst DropdownMenu = DropdownMenuPrimitive.Root\r\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger\r\nconst DropdownMenuContent = React.forwardRef<\r\n React.ElementRef<typeof DropdownMenuPrimitive.Content>,\r\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\r\n>(({ className, sideOffset = 4, ...props }, ref) => (\r\n <DropdownMenuPrimitive.Portal>\r\n <DropdownMenuPrimitive.Content\r\n ref={ref}\r\n sideOffset={sideOffset}\r\n className={cn(\r\n \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\r\n className\r\n )}\r\n {...props}\r\n />\r\n </DropdownMenuPrimitive.Portal>\r\n))\r\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName\r\n\r\nconst DropdownMenuItem = React.forwardRef<\r\n React.ElementRef<typeof DropdownMenuPrimitive.Item>,\r\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\r\n inset?: boolean\r\n }\r\n>(({ className, inset, ...props }, ref) => (\r\n <DropdownMenuPrimitive.Item\r\n ref={ref}\r\n className={cn(\r\n \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\r\n inset && \"pl-8\",\r\n className\r\n )}\r\n {...props}\r\n />\r\n))\r\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName\r\n\r\nconst DropdownMenuLabel = React.forwardRef<\r\n React.ElementRef<typeof DropdownMenuPrimitive.Label>,\r\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\r\n inset?: boolean\r\n }\r\n>(({ className, inset, ...props }, ref) => (\r\n <DropdownMenuPrimitive.Label\r\n ref={ref}\r\n className={cn(\r\n \"px-2 py-1.5 text-sm font-semibold\",\r\n inset && \"pl-8\",\r\n className\r\n )}\r\n {...props}\r\n />\r\n))\r\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName\r\n\r\nconst DropdownMenuSeparator = React.forwardRef<\r\n React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\r\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\r\n>(({ className, ...props }, ref) => (\r\n <DropdownMenuPrimitive.Separator\r\n ref={ref}\r\n className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\r\n {...props}\r\n />\r\n))\r\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName\r\n\r\n// Export ส่วนประกอบอื่นๆ ที่อาจใช้ (Shortcut, Group, Sub) เพื่อความครบถ้วน\r\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group\r\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal\r\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub\r\nconst DropdownMenuSubTrigger = React.forwardRef<\r\n React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\r\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\r\n inset?: boolean\r\n }\r\n>(({ className, inset, children, ...props }, ref) => (\r\n <DropdownMenuPrimitive.SubTrigger\r\n ref={ref}\r\n className={cn(\r\n \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent\",\r\n inset && \"pl-8\",\r\n className\r\n )}\r\n {...props}\r\n >\r\n {children}\r\n <ChevronRight className=\"ml-auto h-4 w-4\" />\r\n </DropdownMenuPrimitive.SubTrigger>\r\n))\r\nDropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName\r\n\r\nconst DropdownMenuSubContent = React.forwardRef<\r\n React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\r\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\r\n>(({ className, ...props }, ref) => (\r\n <DropdownMenuPrimitive.SubContent\r\n ref={ref}\r\n className={cn(\r\n \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\r\n className\r\n )}\r\n {...props}\r\n />\r\n))\r\nDropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName\r\n\r\nconst DropdownMenuShortcut = ({\r\n className,\r\n ...props\r\n}: React.HTMLAttributes<HTMLSpanElement>) => {\r\n return (\r\n <span\r\n className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)}\r\n {...props}\r\n />\r\n )\r\n}\r\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\"\r\n\r\nexport {\r\n DropdownMenu,\r\n DropdownMenuTrigger,\r\n DropdownMenuContent,\r\n DropdownMenuItem,\r\n DropdownMenuLabel,\r\n DropdownMenuSeparator,\r\n DropdownMenuGroup,\r\n DropdownMenuPortal,\r\n DropdownMenuSub,\r\n DropdownMenuSubContent,\r\n DropdownMenuSubTrigger,\r\n DropdownMenuShortcut,\r\n}",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\form.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\hover-card.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\input.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\label.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\popover.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\progress.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\scroll-area.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\select.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\separator.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\sheet.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\skeleton.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\sonner.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\switch.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\table.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\tabs.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\ui\\textarea.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\workflows\\dsl-editor.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'_error' is defined but never used.",
"line": 50,
"column": 14,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 50,
"endColumn": 20
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "'use client';\r\n\r\nimport { useState, useRef, useEffect } from 'react';\r\nimport { Button } from '@/components/ui/button';\r\nimport { Card } from '@/components/ui/card';\r\nimport { Alert, AlertDescription } from '@/components/ui/alert';\r\nimport { CheckCircle, AlertCircle, Play, Loader2 } from 'lucide-react';\r\nimport Editor, { OnMount } from '@monaco-editor/react';\r\nimport { workflowApi } from '@/lib/api/workflows';\r\nimport { ValidationResult } from '@/types/workflow';\r\nimport { useTheme } from 'next-themes';\r\n\r\ninterface DSLEditorProps {\r\n initialValue?: string;\r\n onChange?: (value: string) => void;\r\n readOnly?: boolean;\r\n}\r\n\r\nexport function DSLEditor({ initialValue = '', onChange, readOnly = false }: DSLEditorProps) {\r\n const [dsl, setDsl] = useState(initialValue);\r\n const [validationResult, setValidationResult] = useState<ValidationResult | null>(null);\r\n const [isValidating, setIsValidating] = useState(false);\r\n const editorRef = useRef<unknown>(null);\r\n const { theme } = useTheme();\r\n\r\n // Update internal state if initialValue changes (e.g. loaded from API)\r\n useEffect(() => {\r\n setDsl(initialValue);\r\n }, [initialValue]);\r\n\r\n const handleEditorChange = (value: string | undefined) => {\r\n const newValue = value || '';\r\n setDsl(newValue);\r\n onChange?.(newValue);\r\n // Clear previous validation result on edit to avoid stale state\r\n if (validationResult) {\r\n setValidationResult(null);\r\n }\r\n };\r\n\r\n const handleEditorDidMount: OnMount = (editor) => {\r\n editorRef.current = editor;\r\n };\r\n\r\n const validateDSL = async () => {\r\n setIsValidating(true);\r\n try {\r\n const result = await workflowApi.validateDSL(dsl);\r\n setValidationResult(result);\r\n } catch (_error) {\r\n // Validation failed - error state shown in UI\r\n setValidationResult({ valid: false, errors: ['Validation failed due to server error'] });\r\n } finally {\r\n setIsValidating(false);\r\n }\r\n };\r\n\r\n interface TestResult {\r\n success: boolean;\r\n message: string;\r\n }\r\n\r\n const [testResult, setTestResult] = useState<TestResult | null>(null);\r\n const [isTesting, setIsTesting] = useState(false);\r\n\r\n const testWorkflow = async () => {\r\n setIsTesting(true);\r\n setTestResult(null);\r\n try {\r\n // Mock test execution\r\n await new Promise(resolve => setTimeout(resolve, 1000));\r\n setTestResult({ success: true, message: \"Workflow simulation completed successfully.\" });\r\n } catch {\r\n setTestResult({ success: false, message: \"Workflow simulation failed.\" });\r\n } finally {\r\n setIsTesting(false);\r\n }\r\n };\r\n\r\n return (\r\n <div className=\"space-y-4\">\r\n <div className=\"flex justify-between items-center\">\r\n <h3 className=\"text-lg font-semibold\">Workflow DSL</h3>\r\n <div className=\"flex gap-2\">\r\n <Button\r\n variant=\"outline\"\r\n onClick={validateDSL}\r\n disabled={isValidating || readOnly}\r\n >\r\n {isValidating ? (\r\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\r\n ) : (\r\n <CheckCircle className=\"mr-2 h-4 w-4\" />\r\n )}\r\n Validate\r\n </Button>\r\n <Button variant=\"outline\" onClick={testWorkflow} disabled={isTesting || readOnly}>\r\n {isTesting ? (\r\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\r\n ) : (\r\n <Play className=\"mr-2 h-4 w-4\" />\r\n )}\r\n Test\r\n </Button>\r\n </div>\r\n </div>\r\n\r\n <Card className=\"overflow-hidden border-2\">\r\n <Editor\r\n height=\"500px\"\r\n defaultLanguage=\"yaml\"\r\n value={dsl}\r\n onChange={handleEditorChange}\r\n onMount={handleEditorDidMount}\r\n theme={theme === 'dark' ? 'vs-dark' : 'light'}\r\n options={{\r\n readOnly: readOnly,\r\n minimap: { enabled: false },\r\n fontSize: 14,\r\n lineNumbers: 'on',\r\n rulers: [80],\r\n wordWrap: 'on',\r\n scrollBeyondLastLine: false,\r\n automaticLayout: true,\r\n }}\r\n />\r\n </Card>\r\n\r\n {validationResult && (\r\n <Alert variant={validationResult.valid ? 'default' : 'destructive'} className={validationResult.valid ? \"border-green-500 text-green-700 dark:text-green-400\" : \"\"}>\r\n {validationResult.valid ? (\r\n <CheckCircle className=\"h-4 w-4\" />\r\n ) : (\r\n <AlertCircle className=\"h-4 w-4\" />\r\n )}\r\n <AlertDescription>\r\n {validationResult.valid ? (\r\n <span className=\"font-semibold\">DSL is valid and ready to deploy.</span>\r\n ) : (\r\n <div>\r\n <p className=\"font-medium mb-2\">Validation Errors:</p>\r\n <ul className=\"list-disc list-inside space-y-1\">\r\n {validationResult.errors?.map((error: string, i: number) => (\r\n <li key={i} className=\"text-sm\">\r\n {error}\r\n </li>\r\n ))}\r\n </ul>\r\n </div>\r\n )}\r\n </AlertDescription>\r\n </Alert>\r\n )}\r\n\r\n {testResult && (\r\n <Alert variant={testResult.success ? 'default' : 'destructive'} className={testResult.success ? \"border-blue-500 text-blue-700 dark:text-blue-400\" : \"\"}>\r\n {testResult.success ? <CheckCircle className=\"h-4 w-4\"/> : <AlertCircle className=\"h-4 w-4\"/>}\r\n <AlertDescription>\r\n {testResult.message}\r\n </AlertDescription>\r\n </Alert>\r\n )}\r\n </div>\r\n );\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\components\\workflows\\visual-builder.tsx",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'_e' is defined but never used.",
"line": 238,
"column": 12,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 238,
"endColumn": 14
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "'use client';\r\n\r\nimport { useCallback, useEffect } from 'react';\r\nimport ReactFlow, {\r\n Node,\r\n Edge,\r\n Controls,\r\n Background,\r\n useNodesState,\r\n useEdgesState,\r\n addEdge,\r\n Connection,\r\n ReactFlowProvider,\r\n Panel,\r\n MarkerType,\r\n useReactFlow,\r\n} from 'reactflow';\r\nimport 'reactflow/dist/style.css';\r\n\r\nimport { Button } from '@/components/ui/button';\r\nimport { Plus, Download, Save, Layout } from 'lucide-react';\r\n\r\ninterface WorkflowStateNodeData {\r\n label?: string;\r\n name?: string;\r\n role?: string;\r\n type?: string;\r\n}\r\n\r\ninterface RawTransitionShape {\r\n to?: string;\r\n target?: string;\r\n require?: {\r\n role?: string | string[];\r\n };\r\n}\r\n\r\ninterface RawStateShape {\r\n id?: string;\r\n name: string;\r\n type?: string;\r\n role?: string;\r\n initial?: boolean;\r\n terminal?: boolean;\r\n on?: Record<string, RawTransitionShape>;\r\n}\r\n\r\ninterface CompiledTransitionShape {\r\n to?: string;\r\n target?: string;\r\n requirements?: {\r\n roles?: string[];\r\n };\r\n}\r\n\r\ninterface CompiledStateShape {\r\n initial?: boolean;\r\n terminal?: boolean;\r\n transitions?: Record<string, CompiledTransitionShape>;\r\n}\r\n\r\ninterface ParsedDslShape {\r\n workflow?: string;\r\n initialState?: string;\r\n states?: RawStateShape[] | Record<string, CompiledStateShape>;\r\n dslDefinition?: string;\r\n}\r\n\r\n// Define custom node styles (simplified for now)\r\nconst nodeStyle = {\r\n padding: '10px 20px',\r\n borderRadius: '8px',\r\n border: '1px solid #ddd',\r\n fontSize: '14px',\r\n fontWeight: 500,\r\n background: 'white',\r\n color: '#333',\r\n width: 180, // Increased width for role display\r\n textAlign: 'center' as const,\r\n whiteSpace: 'pre-wrap' as const, // Allow multiline\r\n};\r\n\r\nconst conditionNodeStyle = {\r\n ...nodeStyle,\r\n background: '#fef3c7', // Amber-100\r\n borderColor: '#d97706', // Amber-600\r\n borderStyle: 'dashed',\r\n borderRadius: '24px', // More rounded\r\n};\r\n\r\nconst initialNodes: Node[] = [\r\n {\r\n id: '1',\r\n type: 'input',\r\n data: { label: 'Start' },\r\n position: { x: 250, y: 5 },\r\n style: { ...nodeStyle, background: '#10b981', color: 'white', border: 'none' },\r\n },\r\n];\r\n\r\ninterface VisualWorkflowBuilderProps {\r\n initialNodes?: Node[];\r\n initialEdges?: Edge[];\r\n dslString?: string;\r\n onSave?: (nodes: Node[], edges: Edge[]) => void;\r\n onDslChange?: (dsl: string) => void;\r\n}\r\n\r\nconst createNode = (\r\n name: string,\r\n yOffset: number,\r\n options?: {\r\n isCondition?: boolean;\r\n isStart?: boolean;\r\n isEnd?: boolean;\r\n role?: string;\r\n type?: string;\r\n }\r\n): Node<WorkflowStateNodeData> => {\r\n const isCondition = options?.isCondition === true;\r\n const isStart = options?.isStart === true;\r\n const isEnd = options?.isEnd === true;\r\n\r\n let nodeType: Node['type'] = 'default';\r\n let style = { ...nodeStyle };\r\n\r\n if (isStart) {\r\n nodeType = 'input';\r\n style = { ...nodeStyle, background: '#10b981', color: 'white', border: 'none' };\r\n } else if (isEnd) {\r\n nodeType = 'output';\r\n style = { ...nodeStyle, background: '#ef4444', color: 'white', border: 'none' };\r\n } else if (isCondition) {\r\n style = conditionNodeStyle;\r\n }\r\n\r\n return {\r\n id: name,\r\n type: nodeType,\r\n data: {\r\n label: isStart || isEnd ? name : `${name}\\n(${options?.role || 'No Role'})`,\r\n name,\r\n role: options?.role,\r\n type: options?.type || (isStart ? 'START' : isEnd ? 'END' : 'TASK')\r\n },\r\n position: { x: 250, y: yOffset },\r\n style\r\n };\r\n};\r\n\r\nconst createEdge = (source: string, target: string, label: string): Edge => ({\r\n id: `e-${source}-${label}-${target}`,\r\n source,\r\n target,\r\n label,\r\n markerEnd: { type: MarkerType.ArrowClosed }\r\n});\r\n\r\nfunction parseDSL(dsl: string): { nodes: Node[], edges: Edge[] } {\r\n const nodes: Node[] = [];\r\n const edges: Edge[] = [];\r\n let yOffset = 50;\r\n\r\n try {\r\n const parsedDsl = JSON.parse(dsl) as ParsedDslShape;\r\n\r\n if (typeof parsedDsl.dslDefinition === 'string') {\r\n return parseDSL(parsedDsl.dslDefinition);\r\n }\r\n\r\n if (Array.isArray(parsedDsl.states)) {\r\n parsedDsl.states.forEach((state) => {\r\n const stateName = state.name || state.id || `node-${Date.now()}`;\r\n const role =\r\n state.role ||\r\n (Array.isArray(state.on?.SUBMIT?.require?.role)\r\n ? state.on?.SUBMIT?.require?.role.join(', ')\r\n : state.on?.SUBMIT?.require?.role);\r\n const isCondition = state.type === 'CONDITION';\r\n const isStart = state.initial === true || state.type === 'START';\r\n const isEnd = state.terminal === true || state.type === 'END';\r\n\r\n nodes.push(\r\n createNode(stateName, yOffset, {\r\n isCondition,\r\n isStart,\r\n isEnd,\r\n role,\r\n type: state.type\r\n })\r\n );\r\n\r\n if (state.on) {\r\n Object.entries(state.on).forEach(([eventName, transition]) => {\r\n const target = transition?.to || transition?.target;\r\n if (target) {\r\n edges.push(createEdge(stateName, target, eventName));\r\n }\r\n });\r\n }\r\n\r\n yOffset += 120;\r\n });\r\n\r\n return { nodes, edges };\r\n }\r\n\r\n if (parsedDsl.states && typeof parsedDsl.states === 'object') {\r\n Object.entries(parsedDsl.states).forEach(([stateName, state]) => {\r\n const roles = state.transitions\r\n ? Object.values(state.transitions)\r\n .flatMap((transition) => transition.requirements?.roles || [])\r\n .filter((role, index, array) => array.indexOf(role) === index)\r\n : [];\r\n const isStart = parsedDsl.initialState === stateName || state.initial === true;\r\n const isEnd = state.terminal === true;\r\n\r\n nodes.push(\r\n createNode(stateName, yOffset, {\r\n isStart,\r\n isEnd,\r\n role: roles.join(', ')\r\n })\r\n );\r\n\r\n if (state.transitions) {\r\n Object.entries(state.transitions).forEach(([eventName, transition]) => {\r\n const target = transition?.to || transition?.target;\r\n if (target) {\r\n edges.push(createEdge(stateName, target, eventName));\r\n }\r\n });\r\n }\r\n\r\n yOffset += 120;\r\n });\r\n }\r\n } catch (_e) {\r\n // Failed to parse DSL as JSON - nodes/edges remain empty\r\n }\r\n\r\n return { nodes, edges };\r\n}\r\n\r\nfunction VisualWorkflowBuilderContent({ initialNodes: propNodes, initialEdges: propEdges, dslString, onSave, onDslChange }: VisualWorkflowBuilderProps) {\r\n const [nodes, setNodes, onNodesChange] = useNodesState(propNodes || initialNodes);\r\n const [edges, setEdges, onEdgesChange] = useEdgesState(propEdges || []);\r\n const { fitView } = useReactFlow();\r\n\r\n // Sync DSL to nodes when dslString changes\r\n useEffect(() => {\r\n if (dslString) {\r\n const { nodes: newNodes, edges: newEdges } = parseDSL(dslString);\r\n setNodes(newNodes.length > 0 ? newNodes : propNodes || initialNodes);\r\n setEdges(newNodes.length > 0 ? newEdges : propEdges || []);\r\n setTimeout(() => fitView(), 100);\r\n }\r\n }, [dslString, fitView, propEdges, propNodes, setEdges, setNodes]);\r\n\r\n const onConnect = useCallback(\r\n (params: Connection) => setEdges((eds) => addEdge({ ...params, markerEnd: { type: MarkerType.ArrowClosed } }, eds)),\r\n [setEdges]\r\n );\r\n\r\n const addNode = (type: string, label: string) => {\r\n const id = `${type}-${Date.now()}`;\r\n const nodeType = type === 'condition' ? 'CONDITION' : type === 'end' ? 'END' : type === 'start' ? 'START' : 'TASK';\r\n\r\n const newNode: Node<WorkflowStateNodeData> = {\r\n id,\r\n position: { x: Math.random() * 400 + 100, y: Math.random() * 300 + 100 },\r\n data: { label: label, name: label, role: 'User', type: nodeType },\r\n style: { ...nodeStyle },\r\n };\r\n\r\n if (type === 'end') {\r\n newNode.style = { ...nodeStyle, background: '#ef4444', color: 'white', border: 'none' };\r\n newNode.type = 'output';\r\n } else if (type === 'start') {\r\n newNode.style = { ...nodeStyle, background: '#10b981', color: 'white', border: 'none' };\r\n newNode.type = 'input';\r\n } else if (type === 'condition') {\r\n newNode.style = conditionNodeStyle;\r\n }\r\n\r\n setNodes((nds) => nds.concat(newNode));\r\n };\r\n\r\n const handleSave = () => {\r\n onSave?.(nodes, edges);\r\n };\r\n\r\n // Generate JSON DSL\r\n const generateDSL = () => {\r\n const states = nodes.map(n => {\r\n const outgoingEdges = edges.filter(e => e.source === n.id);\r\n const onConfig: Record<string, { to: string }> = {};\r\n\r\n outgoingEdges.forEach(e => {\r\n const eventName = e.label || 'PROCEED';\r\n onConfig[eventName as string] = { to: e.target };\r\n });\r\n\r\n const isStartNode = n.type === 'input';\r\n const isEndNode = n.type === 'output';\r\n const nodeData = n.data as WorkflowStateNodeData;\r\n\r\n const stateObj: { name: string; type?: string; role?: string; initial?: boolean; terminal?: boolean; on?: Record<string, { to: string }> } = {\r\n name: nodeData.name || nodeData.label?.split('\\n')[0] || n.id,\r\n };\r\n\r\n if (nodeData.type && nodeData.type !== 'START' && nodeData.type !== 'END' && nodeData.type !== 'TASK') {\r\n stateObj.type = nodeData.type;\r\n }\r\n\r\n if (nodeData.role && !isStartNode && !isEndNode) {\r\n stateObj.role = nodeData.role;\r\n }\r\n\r\n if (isStartNode) {\r\n stateObj.initial = true;\r\n }\r\n if (isEndNode) {\r\n stateObj.terminal = true;\r\n }\r\n if (Object.keys(onConfig).length > 0) {\r\n stateObj.on = onConfig;\r\n }\r\n\r\n return stateObj;\r\n });\r\n\r\n const dslObj = {\r\n workflow: \"VISUAL_WORKFLOW\",\r\n version: 1,\r\n states\r\n };\r\n const dsl = JSON.stringify(dslObj, null, 2);\r\n\r\n // DSL generated from visual builder\r\n onDslChange?.(dsl);\r\n alert(\"DSL Updated from Visual Builder!\");\r\n };\r\n\r\n return (\r\n <div className=\"space-y-4 h-full flex flex-col\">\r\n <div className=\"h-[600px] border rounded-lg overflow-hidden relative bg-slate-50 dark:bg-slate-950\">\r\n <ReactFlow\r\n nodes={nodes}\r\n edges={edges}\r\n onNodesChange={onNodesChange}\r\n onEdgesChange={onEdgesChange}\r\n onConnect={onConnect}\r\n fitView\r\n attributionPosition=\"bottom-right\"\r\n >\r\n <Controls />\r\n <Background color=\"#aaa\" gap={16} />\r\n\r\n <Panel position=\"top-right\" className=\"flex gap-2 p-2 bg-white/80 dark:bg-black/50 rounded-lg backdrop-blur-sm border shadow-sm\">\r\n <Button size=\"sm\" variant=\"secondary\" onClick={() => addNode('step', 'New Step')}>\r\n <Plus className=\"mr-2 h-4 w-4\" /> Add Step\r\n </Button>\r\n <Button size=\"sm\" variant=\"secondary\" onClick={() => addNode('condition', 'Condition')}>\r\n <Layout className=\"mr-2 h-4 w-4\" /> Condition\r\n </Button>\r\n <Button size=\"sm\" variant=\"secondary\" onClick={() => addNode('end', 'End')}>\r\n <Plus className=\"mr-2 h-4 w-4\" /> Add End\r\n </Button>\r\n </Panel>\r\n\r\n <Panel position=\"bottom-left\" className=\"flex gap-2\">\r\n <Button size=\"sm\" onClick={handleSave}>\r\n <Save className=\"mr-2 h-4 w-4\" /> Save Visual State\r\n </Button>\r\n <Button size=\"sm\" variant=\"outline\" onClick={generateDSL}>\r\n <Download className=\"mr-2 h-4 w-4\" /> Generate DSL\r\n </Button>\r\n </Panel>\r\n </ReactFlow>\r\n </div>\r\n <div className=\"text-sm text-muted-foreground\">\r\n <p>Tip: Drag to connect nodes. Use backspace to delete selected nodes.</p>\r\n </div>\r\n </div>\r\n );\r\n}\r\n\r\nexport function VisualWorkflowBuilder(props: VisualWorkflowBuilderProps) {\r\n return (\r\n <ReactFlowProvider>\r\n <VisualWorkflowBuilderContent {...props} />\r\n </ReactFlowProvider>\r\n )\r\n}\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\eslint.config.mjs",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\hooks\\__tests__\\use-correspondence.test.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\hooks\\__tests__\\use-drawing.test.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\hooks\\__tests__\\use-projects.test.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\hooks\\__tests__\\use-rfa.test.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\hooks\\__tests__\\use-users.test.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\hooks\\use-audit-logs.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\hooks\\use-correspondence.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\hooks\\use-dashboard.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\hooks\\use-drawing.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\hooks\\use-master-data.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\hooks\\use-notification.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\hooks\\use-numbering.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\hooks\\use-projects.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\hooks\\use-reference-data.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\hooks\\use-rfa.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\hooks\\use-search.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\hooks\\use-users.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\hooks\\use-workflows.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\api\\admin.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\api\\client.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'_error' is defined but never used.",
"line": 43,
"column": 16,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 43,
"endColumn": 22
}
],
"suppressedMessages": [],
"errorCount": 1,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "// File: lib/api/client.ts\r\nimport axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosError } from \"axios\";\r\nimport { v4 as uuidv4 } from \"uuid\";\r\n\r\n// อ่านค่า Base URL จาก Environment Variable\r\nconst baseURL = process.env.NEXT_PUBLIC_API_URL || \"http://localhost:3001/api\";\r\n\r\n// สร้าง Axios Instance หลัก\r\nconst apiClient: AxiosInstance = axios.create({\r\n baseURL,\r\n headers: {\r\n \"Content-Type\": \"application/json\",\r\n },\r\n timeout: 15000, // Timeout 15 วินาที\r\n});\r\n\r\n// ---------------------------------------------------------------------------\r\n// Request Interceptors\r\n// ---------------------------------------------------------------------------\r\n\r\napiClient.interceptors.request.use(\r\n async (config: InternalAxiosRequestConfig) => {\r\n // 1. Idempotency Key Injection\r\n // ป้องกันการทำรายการซ้ำสำหรับ Method ที่เปลี่ยนแปลงข้อมูล\r\n const method = config.method?.toLowerCase();\r\n if (method && [\"post\", \"put\", \"delete\", \"patch\"].includes(method)) {\r\n config.headers[\"Idempotency-Key\"] = uuidv4();\r\n }\r\n\r\n // 2. Authentication Token Injection\r\n // ดึง Token จาก Zustand persist store (localStorage)\r\n if (typeof window !== \"undefined\") {\r\n try {\r\n const authStorage = localStorage.getItem('auth-storage');\r\n if (authStorage) {\r\n const parsed = JSON.parse(authStorage);\r\n const token = parsed?.state?.token;\r\n\r\n if (token) {\r\n config.headers[\"Authorization\"] = `Bearer ${token}`;\r\n }\r\n }\r\n } catch (_error) {\r\n // Auth token retrieval failed - request will proceed without token\r\n }\r\n }\r\n\r\n return config;\r\n },\r\n (error) => {\r\n return Promise.reject(error);\r\n }\r\n);\r\n\r\n// ---------------------------------------------------------------------------\r\n// Response Interceptors\r\n// ---------------------------------------------------------------------------\r\n\r\napiClient.interceptors.response.use(\r\n (response) => {\r\n return response;\r\n },\r\n (error: AxiosError) => {\r\n if (error.response) {\r\n const { status } = error.response;\r\n\r\n // กรณี Token หมดอายุ หรือ ไม่มีสิทธิ์\r\n if (status === 401) {\r\n // Unauthorized: redirect handled by auth interceptor\r\n // สามารถเพิ่ม Logic Redirect ไปหน้า Login ได้ถ้าต้องการ\r\n }\r\n }\r\n return Promise.reject(error);\r\n }\r\n);\r\n\r\nexport default apiClient;\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\api\\dashboard.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\api\\drawings.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\api\\notifications.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\api\\numbering.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\api\\workflows.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\auth.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'error' is defined but never used.",
"line": 106,
"column": 12,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 106,
"endColumn": 17
},
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'errorBody' is assigned a value but never used. Allowed unused vars must match /^_/u.",
"line": 154,
"column": 19,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 154,
"endColumn": 28
},
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'error' is defined but never used.",
"line": 180,
"column": 18,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 180,
"endColumn": 23
}
],
"suppressedMessages": [],
"errorCount": 3,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "// File: lib/auth.ts\r\nimport NextAuth from \"next-auth\";\r\nimport Credentials from \"next-auth/providers/credentials\";\r\nimport { z } from \"zod\";\r\nimport type { User } from \"next-auth\";\r\nimport type { JWT } from \"next-auth/jwt\";\r\n\r\n// Schema for input validation\r\nconst _loginSchema = z.object({\r\n username: z.string().min(1),\r\n password: z.string().min(1),\r\n});\r\n\r\nconst baseUrl = (typeof window === \"undefined\" ? process.env.INTERNAL_API_URL : null) || process.env.NEXT_PUBLIC_API_URL || \"http://localhost:3001/api\";\r\n\r\n// Helper to parse JWT expiry\r\nfunction getJwtExpiry(token: string): number {\r\n try {\r\n const payload = JSON.parse(atob(token.split('.')[1]));\r\n return payload.exp * 1000; // Convert to ms\r\n } catch {\r\n return Date.now(); // If invalid, treat as expired\r\n }\r\n}\r\n\r\ninterface TokenPayload {\r\n access_token: string;\r\n refresh_token?: string;\r\n}\r\n\r\ninterface LoginPayload extends TokenPayload {\r\n user: {\r\n user_id: number;\r\n username: string;\r\n email?: string;\r\n firstName?: string;\r\n lastName?: string;\r\n role?: string;\r\n primaryOrganizationId?: number;\r\n };\r\n}\r\n\r\nfunction unwrapApiResponse(value: unknown): unknown {\r\n let current = value;\r\n\r\n for (let i = 0; i < 5; i += 1) {\r\n if (!current || typeof current !== \"object\") {\r\n return current;\r\n }\r\n\r\n const record = current as Record<string, unknown>;\r\n if (typeof record.access_token === \"string\") {\r\n return current;\r\n }\r\n\r\n if (!(\"data\" in record)) {\r\n return current;\r\n }\r\n\r\n current = record.data;\r\n }\r\n\r\n return current;\r\n}\r\n\r\nfunction isTokenPayload(value: unknown): value is TokenPayload {\r\n return !!value && typeof value === \"object\" && typeof (value as Record<string, unknown>).access_token === \"string\";\r\n}\r\n\r\nfunction isLoginPayload(value: unknown): value is LoginPayload {\r\n if (!isTokenPayload(value)) {\r\n return false;\r\n }\r\n\r\n const user = (value as unknown as { user?: unknown }).user;\r\n return !!user && typeof user === \"object\" && typeof (user as Record<string, unknown>).username === \"string\";\r\n}\r\n\r\nasync function refreshAccessToken(token: JWT) {\r\n try {\r\n const response = await fetch(`${baseUrl}/auth/refresh`, {\r\n method: \"POST\",\r\n headers: {\r\n Authorization: `Bearer ${token.refreshToken}`,\r\n },\r\n });\r\n\r\n const refreshedTokens = await response.json();\r\n\r\n if (!response.ok) {\r\n throw refreshedTokens;\r\n }\r\n\r\n const data = unwrapApiResponse(refreshedTokens);\r\n\r\n if (!isTokenPayload(data)) {\r\n throw new Error(\"Invalid refresh response format\");\r\n }\r\n\r\n return {\r\n ...token,\r\n accessToken: data.access_token,\r\n accessTokenExpires: getJwtExpiry(data.access_token),\r\n refreshToken: data.refresh_token ?? token.refreshToken,\r\n };\r\n } catch (error) {\r\n // RefreshAccessTokenError - token will be invalidated\r\n\r\n return {\r\n ...token,\r\n error: \"RefreshAccessTokenError\",\r\n };\r\n }\r\n}\r\n\r\nexport const {\r\n handlers: { GET, POST },\r\n auth,\r\n signIn,\r\n signOut,\r\n} = NextAuth({\r\n providers: [\r\n Credentials({\r\n name: \"Credentials\",\r\n credentials: {\r\n username: { label: \"Username\", type: \"text\" },\r\n password: { label: \"Password\", type: \"password\" },\r\n },\r\n authorize: async (credentials) => {\r\n if (!credentials?.username || !credentials?.password) return null;\r\n\r\n try {\r\n // 1. Sanitize payload (Only send username and password)\r\n const payload = {\r\n username: credentials.username as string,\r\n password: credentials.password as string,\r\n };\r\n\r\n // console.log(`[AUTH] Attempting login at: ${baseUrl}/auth/login`); /* TODO: Remove before prod */\r\n // console.log(`[AUTH] Current process.env.INTERNAL_API_URL: ${process.env.INTERNAL_API_URL}`); /* TODO: Remove before prod */\r\n // console.log(`[AUTH] Current process.env.NEXT_PUBLIC_API_URL: ${process.env.NEXT_PUBLIC_API_URL}`); /* TODO: Remove before prod */\r\n\r\n const res = await fetch(`${baseUrl}/auth/login`, {\r\n method: \"POST\",\r\n body: JSON.stringify(payload),\r\n headers: {\r\n \"Content-Type\": \"application/json\",\r\n },\r\n cache: 'no-store', // Disable caching for auth requests\r\n });\r\n\r\n if (!res.ok) {\r\n // console.error(`[AUTH] Login Failed: status ${res.status}`); /* TODO: Remove before prod */\r\n const errorBody = await res.text().catch(() => \"No error body\");\r\n // console.error(`[AUTH] Error details: ${errorBody}`); /* TODO: Remove before prod */\r\n return null;\r\n }\r\n\r\n const data = await res.json();\r\n const backendData = unwrapApiResponse(data);\r\n\r\n if (!isLoginPayload(backendData)) {\r\n // console.error(\"[AUTH] Login failed: Invalid response format from backend (missing access_token)\"); /* TODO: Remove before prod */\r\n return null;\r\n }\r\n\r\n // console.log(`[AUTH] Login Successful for user: ${backendData.user?.username || 'unknown'}`); /* TODO: Remove before prod */\r\n\r\n return {\r\n id: backendData.user.user_id.toString(),\r\n name: `${backendData.user.firstName ?? \"\"} ${backendData.user.lastName ?? \"\"}`.trim(),\r\n email: backendData.user.email,\r\n username: backendData.user.username,\r\n role: backendData.user.role || \"User\",\r\n organizationId: backendData.user.primaryOrganizationId,\r\n accessToken: backendData.access_token,\r\n refreshToken: backendData.refresh_token,\r\n } as User;\r\n\r\n } catch (error) {\r\n // console.error(\"[AUTH] Network/Fetch Error during authorize:\", error); /* TODO: Remove before prod */\r\n return null;\r\n }\r\n },\r\n }),\r\n ],\r\n pages: {\r\n signIn: \"/login\",\r\n error: \"/login\",\r\n },\r\n callbacks: {\r\n async jwt({ token, user }) {\r\n if (user) {\r\n return {\r\n ...token,\r\n id: user.id,\r\n username: user.username, // ✅ Save username\r\n role: user.role,\r\n organizationId: user.organizationId,\r\n accessToken: user.accessToken,\r\n refreshToken: user.refreshToken,\r\n accessTokenExpires: getJwtExpiry(user.accessToken!),\r\n };\r\n }\r\n\r\n // Return previous token if valid (minus 10s buffer)\r\n if (Date.now() < (token.accessTokenExpires as number) - 10000) {\r\n return token;\r\n }\r\n\r\n // If existing token has an error, do not retry refresh (prevents infinite loop)\r\n if (token.error) {\r\n return token;\r\n }\r\n\r\n // Token expired, refresh it\r\n return refreshAccessToken(token);\r\n },\r\n async session({ session, token }) {\r\n if (token && session.user) {\r\n session.user.id = token.id as string;\r\n session.user.username = token.username as string; // ✅ Restore username\r\n session.user.role = token.role as string;\r\n session.user.organizationId = token.organizationId as number;\r\n\r\n session.accessToken = token.accessToken as string;\r\n session.refreshToken = token.refreshToken as string;\r\n session.error = token.error as string;\r\n }\r\n return session;\r\n },\r\n },\r\n session: {\r\n strategy: \"jwt\",\r\n maxAge: 24 * 60 * 60, // 24 hours\r\n },\r\n secret: process.env.AUTH_SECRET,\r\n debug: process.env.NODE_ENV === \"development\",\r\n});\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\__tests__\\correspondence.service.test.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\__tests__\\master-data.service.test.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\__tests__\\project.service.test.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\asbuilt-drawing.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\audit-log.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\circulation.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\contract-drawing.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\contract.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\correspondence.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\dashboard.service.ts",
"messages": [
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'_error' is defined but never used.",
"line": 18,
"column": 14,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 18,
"endColumn": 20
},
{
"ruleId": "@typescript-eslint/no-unused-vars",
"severity": 2,
"message": "'_error' is defined but never used.",
"line": 34,
"column": 14,
"nodeType": "Identifier",
"messageId": "unusedVar",
"endLine": 34,
"endColumn": 20
}
],
"suppressedMessages": [],
"errorCount": 2,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"source": "import apiClient from \"@/lib/api/client\";\r\nimport { DashboardStats, ActivityLog, PendingTask } from \"@/types/dashboard\";\r\n\r\nexport const dashboardService = {\r\n getStats: async (): Promise<DashboardStats> => {\r\n const response = await apiClient.get(\"/dashboard/stats\");\r\n return response.data;\r\n },\r\n\r\n getRecentActivity: async (): Promise<ActivityLog[]> => {\r\n try {\r\n const response = await apiClient.get(\"/dashboard/activity\");\r\n // ตรวจสอบว่า response.data เป็น array จริงๆ\r\n if (Array.isArray(response.data)) {\r\n return response.data;\r\n }\r\n return [];\r\n } catch (_error) {\r\n return [];\r\n }\r\n },\r\n\r\n getPendingTasks: async (): Promise<PendingTask[]> => {\r\n try {\r\n const response = await apiClient.get(\"/dashboard/pending\");\r\n // Backend คืน { data: [], meta: {} } ต้องดึง data ออกมา\r\n if (response.data?.data && Array.isArray(response.data.data)) {\r\n return response.data.data;\r\n }\r\n if (Array.isArray(response.data)) {\r\n return response.data;\r\n }\r\n return [];\r\n } catch (_error) {\r\n return [];\r\n }\r\n },\r\n};\r\n",
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\document-numbering.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\drawing-master-data.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\index.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\json-schema.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\master-data.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\migration.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\monitoring.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\notification.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\organization.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\project.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\rfa.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\search.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\session.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\shop-drawing.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\transmittal.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\user.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\services\\workflow-engine.service.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\stores\\auth-store.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\stores\\draft-store.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\stores\\ui-store.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\test-utils.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\utils.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\lib\\utils\\uuid-guard.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\next-env.d.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\next.config.mjs",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\postcss.config.mjs",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\providers\\query-provider.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\providers\\session-provider.tsx",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\proxy.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\tailwind.config.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\admin.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\api-error.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\circulation.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\correspondence.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dashboard.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\drawing.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\circulation\\create-circulation.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\circulation\\search-circulation.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\circulation\\update-circulation-routing.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\contract\\contract.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\correspondence\\add-reference.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\correspondence\\create-correspondence.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\correspondence\\search-correspondence.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\correspondence\\submit-correspondence.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\correspondence\\workflow-action.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\drawing\\asbuilt-drawing.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\drawing\\contract-drawing.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\drawing\\shop-drawing.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\json-schema\\json-schema.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\master\\correspondence-type.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\master\\discipline.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\master\\number-format.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\master\\rfa-type.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\master\\sub-type.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\master\\tag.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\monitoring\\set-maintenance.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\notification\\notification.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\numbering.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\organization\\organization.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\project\\project.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\rfa\\rfa.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\search\\search-query.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\transmittal\\transmittal.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\user\\user.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\dto\\workflow-engine\\workflow-engine.dto.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\migration.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\next-auth.d.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\notification.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\numbering.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\organization.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\react-day-picker.d.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\rfa.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\search.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\transmittal.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\user.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\types\\workflow.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\vitest.config.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\frontend\\vitest.setup.ts",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
},
{
"filePath": "D:\\nap-dms.lcbp3\\test_preview.js",
"messages": [],
"suppressedMessages": [],
"errorCount": 0,
"fatalErrorCount": 0,
"warningCount": 0,
"fixableErrorCount": 0,
"fixableWarningCount": 0,
"usedDeprecatedRules": []
}
]