This commit is contained in:
@@ -3,20 +3,45 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
ConflictException,
|
ConflictException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository, InjectEntityManager } from '@nestjs/typeorm';
|
||||||
import { Repository, Like } from 'typeorm';
|
import { Repository, Like, EntityManager } from 'typeorm';
|
||||||
import { Contract } from './entities/contract.entity';
|
import { Contract } from './entities/contract.entity';
|
||||||
import { CreateContractDto } from './dto/create-contract.dto.js';
|
import { CreateContractDto } from './dto/create-contract.dto.js';
|
||||||
import { UpdateContractDto } from './dto/update-contract.dto.js';
|
import { UpdateContractDto } from './dto/update-contract.dto.js';
|
||||||
|
import { Project } from '../project/entities/project.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ContractService {
|
export class ContractService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Contract)
|
@InjectRepository(Contract)
|
||||||
private readonly contractRepo: Repository<Contract>
|
private readonly contractRepo: Repository<Contract>,
|
||||||
|
@InjectEntityManager()
|
||||||
|
private readonly entityManager: EntityManager
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to resolve projectId (ID or UUID) to internal INT ID
|
||||||
|
*/
|
||||||
|
async resolveProjectId(projectId: number | string): Promise<number> {
|
||||||
|
if (typeof projectId === 'number') return projectId;
|
||||||
|
const num = Number(projectId);
|
||||||
|
if (!isNaN(num)) return num;
|
||||||
|
|
||||||
|
const project = await this.entityManager.findOne(Project, {
|
||||||
|
where: { uuid: projectId as string },
|
||||||
|
select: ['id'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw new NotFoundException(`Project with UUID ${projectId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return project.id;
|
||||||
|
}
|
||||||
|
|
||||||
async create(dto: CreateContractDto) {
|
async create(dto: CreateContractDto) {
|
||||||
|
const internalProjectId = await this.resolveProjectId(dto.projectId);
|
||||||
|
|
||||||
const existing = await this.contractRepo.findOne({
|
const existing = await this.contractRepo.findOne({
|
||||||
where: { contractCode: dto.contractCode },
|
where: { contractCode: dto.contractCode },
|
||||||
});
|
});
|
||||||
@@ -25,7 +50,7 @@ export class ContractService {
|
|||||||
`Contract Code "${dto.contractCode}" already exists`
|
`Contract Code "${dto.contractCode}" already exists`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const contract = this.contractRepo.create(dto);
|
const contract = this.contractRepo.create({ ...dto, projectId: internalProjectId });
|
||||||
return this.contractRepo.save(contract);
|
return this.contractRepo.save(contract);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +58,11 @@ export class ContractService {
|
|||||||
const { search, projectId, page = 1, limit = 100 } = params || {};
|
const { search, projectId, page = 1, limit = 100 } = params || {};
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
let internalProjectId = undefined;
|
||||||
|
if (projectId) {
|
||||||
|
internalProjectId = await this.resolveProjectId(projectId);
|
||||||
|
}
|
||||||
|
|
||||||
const findOptions: any = {
|
const findOptions: any = {
|
||||||
relations: ['project'],
|
relations: ['project'],
|
||||||
order: { contractCode: 'ASC' },
|
order: { contractCode: 'ASC' },
|
||||||
@@ -47,21 +77,20 @@ export class ContractService {
|
|||||||
searchConditions.push({ contractName: Like(`%${search}%`) });
|
searchConditions.push({ contractName: Like(`%${search}%`) });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (projectId) {
|
if (internalProjectId) {
|
||||||
// Combine project filter with search if exists
|
|
||||||
if (searchConditions.length > 0) {
|
if (searchConditions.length > 0) {
|
||||||
findOptions.where = searchConditions.map((cond) => ({
|
findOptions.where = searchConditions.map((cond) => ({
|
||||||
...cond,
|
...cond,
|
||||||
projectId,
|
projectId: internalProjectId,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
findOptions.where = { projectId };
|
findOptions.where = { projectId: internalProjectId };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (searchConditions.length > 0) {
|
if (searchConditions.length > 0) {
|
||||||
findOptions.where = searchConditions;
|
findOptions.where = searchConditions;
|
||||||
} else {
|
} else {
|
||||||
delete findOptions.where; // No filters
|
delete findOptions.where;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +128,9 @@ export class ContractService {
|
|||||||
|
|
||||||
async update(uuid: string, dto: UpdateContractDto) {
|
async update(uuid: string, dto: UpdateContractDto) {
|
||||||
const contract = await this.findOneByUuid(uuid);
|
const contract = await this.findOneByUuid(uuid);
|
||||||
|
if (dto.projectId) {
|
||||||
|
dto.projectId = await this.resolveProjectId(dto.projectId);
|
||||||
|
}
|
||||||
Object.assign(contract, dto);
|
Object.assign(contract, dto);
|
||||||
return this.contractRepo.save(contract);
|
return this.contractRepo.save(contract);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
BeforeInsert,
|
BeforeInsert,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { v7 as uuidv7 } from 'uuid';
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
import { Exclude } from 'class-transformer';
|
import { Exclude, Expose } from 'class-transformer';
|
||||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||||
import { Project } from '../../project/entities/project.entity';
|
import { Project } from '../../project/entities/project.entity';
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ export class Contract extends BaseEntity {
|
|||||||
@Exclude()
|
@Exclude()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
|
@Expose({ name: 'id' })
|
||||||
@Column({
|
@Column({
|
||||||
type: 'uuid',
|
type: 'uuid',
|
||||||
unique: true,
|
unique: true,
|
||||||
|
|||||||
@@ -38,28 +38,16 @@ export class DrawingMasterDataController {
|
|||||||
|
|
||||||
@Get('contract/volumes')
|
@Get('contract/volumes')
|
||||||
@ApiOperation({ summary: 'List Contract Drawing Volumes' })
|
@ApiOperation({ summary: 'List Contract Drawing Volumes' })
|
||||||
@ApiQuery({ name: 'projectId', required: true, type: Number })
|
@ApiQuery({ name: 'projectId', required: true, type: String })
|
||||||
@RequirePermission('document.view')
|
@RequirePermission('document.view')
|
||||||
getVolumes(@Query('projectId', ParseIntPipe) projectId: number) {
|
getVolumes(@Query('projectId') projectId: string | number) {
|
||||||
this.logger.log(`Fetching Contract Volumes for Project ID: ${projectId}`);
|
|
||||||
return this.masterDataService.findAllVolumes(projectId);
|
return this.masterDataService.findAllVolumes(projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... (Create/Update/Delete methods remain unchanged) ...
|
|
||||||
|
|
||||||
@Post('contract/volumes')
|
@Post('contract/volumes')
|
||||||
@ApiOperation({ summary: 'Create Volume' })
|
@ApiOperation({ summary: 'Create Volume' })
|
||||||
@RequirePermission('master_data.drawing_category.manage')
|
@RequirePermission('master_data.drawing_category.manage')
|
||||||
createVolume(
|
createVolume(@Body() body: any) {
|
||||||
@Body()
|
|
||||||
body: {
|
|
||||||
projectId: number;
|
|
||||||
volumeCode: string;
|
|
||||||
volumeName: string;
|
|
||||||
description?: string;
|
|
||||||
sortOrder: number;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return this.masterDataService.createVolume(body);
|
return this.masterDataService.createVolume(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,13 +56,7 @@ export class DrawingMasterDataController {
|
|||||||
@RequirePermission('master_data.drawing_category.manage')
|
@RequirePermission('master_data.drawing_category.manage')
|
||||||
updateVolume(
|
updateVolume(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Body()
|
@Body() body: any
|
||||||
body: {
|
|
||||||
volumeCode?: string;
|
|
||||||
volumeName?: string;
|
|
||||||
description?: string;
|
|
||||||
sortOrder?: number;
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
return this.masterDataService.updateVolume(id, body);
|
return this.masterDataService.updateVolume(id, body);
|
||||||
}
|
}
|
||||||
@@ -92,30 +74,16 @@ export class DrawingMasterDataController {
|
|||||||
|
|
||||||
@Get('contract/categories')
|
@Get('contract/categories')
|
||||||
@ApiOperation({ summary: 'List Contract Drawing Categories' })
|
@ApiOperation({ summary: 'List Contract Drawing Categories' })
|
||||||
@ApiQuery({ name: 'projectId', required: true, type: Number })
|
@ApiQuery({ name: 'projectId', required: true, type: String })
|
||||||
@RequirePermission('document.view')
|
@RequirePermission('document.view')
|
||||||
getCategories(@Query('projectId', ParseIntPipe) projectId: number) {
|
getCategories(@Query('projectId') projectId: string | number) {
|
||||||
this.logger.log(
|
|
||||||
`Fetching Contract Categories for Project ID: ${projectId}`
|
|
||||||
);
|
|
||||||
return this.masterDataService.findAllCategories(projectId);
|
return this.masterDataService.findAllCategories(projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... (Create/Update/Delete methods remain unchanged) ...
|
|
||||||
|
|
||||||
@Post('contract/categories')
|
@Post('contract/categories')
|
||||||
@ApiOperation({ summary: 'Create Category' })
|
@ApiOperation({ summary: 'Create Category' })
|
||||||
@RequirePermission('master_data.drawing_category.manage')
|
@RequirePermission('master_data.drawing_category.manage')
|
||||||
createCategory(
|
createCategory(@Body() body: any) {
|
||||||
@Body()
|
|
||||||
body: {
|
|
||||||
projectId: number;
|
|
||||||
catCode: string;
|
|
||||||
catName: string;
|
|
||||||
description?: string;
|
|
||||||
sortOrder: number;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return this.masterDataService.createCategory(body);
|
return this.masterDataService.createCategory(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,13 +92,7 @@ export class DrawingMasterDataController {
|
|||||||
@RequirePermission('master_data.drawing_category.manage')
|
@RequirePermission('master_data.drawing_category.manage')
|
||||||
updateCategory(
|
updateCategory(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Body()
|
@Body() body: any
|
||||||
body: {
|
|
||||||
catCode?: string;
|
|
||||||
catName?: string;
|
|
||||||
description?: string;
|
|
||||||
sortOrder?: number;
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
return this.masterDataService.updateCategory(id, body);
|
return this.masterDataService.updateCategory(id, body);
|
||||||
}
|
}
|
||||||
@@ -148,30 +110,16 @@ export class DrawingMasterDataController {
|
|||||||
|
|
||||||
@Get('contract/sub-categories')
|
@Get('contract/sub-categories')
|
||||||
@ApiOperation({ summary: 'List Contract Drawing Sub-Categories' })
|
@ApiOperation({ summary: 'List Contract Drawing Sub-Categories' })
|
||||||
@ApiQuery({ name: 'projectId', required: true, type: Number })
|
@ApiQuery({ name: 'projectId', required: true, type: String })
|
||||||
@RequirePermission('document.view')
|
@RequirePermission('document.view')
|
||||||
getContractSubCats(@Query('projectId', ParseIntPipe) projectId: number) {
|
getContractSubCats(@Query('projectId') projectId: string | number) {
|
||||||
this.logger.log(
|
|
||||||
`Fetching Contract Sub-Categories for Project ID: ${projectId}`
|
|
||||||
);
|
|
||||||
return this.masterDataService.findAllContractSubCats(projectId);
|
return this.masterDataService.findAllContractSubCats(projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... (Create/Update/Delete methods remain unchanged) ...
|
|
||||||
|
|
||||||
@Post('contract/sub-categories')
|
@Post('contract/sub-categories')
|
||||||
@ApiOperation({ summary: 'Create Contract Sub-Category' })
|
@ApiOperation({ summary: 'Create Contract Sub-Category' })
|
||||||
@RequirePermission('master_data.drawing_category.manage')
|
@RequirePermission('master_data.drawing_category.manage')
|
||||||
createContractSubCat(
|
createContractSubCat(@Body() body: any) {
|
||||||
@Body()
|
|
||||||
body: {
|
|
||||||
projectId: number;
|
|
||||||
subCatCode: string;
|
|
||||||
subCatName: string;
|
|
||||||
description?: string;
|
|
||||||
sortOrder: number;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return this.masterDataService.createContractSubCat(body);
|
return this.masterDataService.createContractSubCat(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,18 +128,15 @@ export class DrawingMasterDataController {
|
|||||||
@RequirePermission('master_data.drawing_category.manage')
|
@RequirePermission('master_data.drawing_category.manage')
|
||||||
updateContractSubCat(
|
updateContractSubCat(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Body()
|
@Body() body: any
|
||||||
body: {
|
|
||||||
subCatCode?: string;
|
|
||||||
subCatName?: string;
|
|
||||||
description?: string;
|
|
||||||
sortOrder?: number;
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
return this.masterDataService.updateContractSubCat(id, body);
|
return this.masterDataService.updateContractSubCat(id, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteContractSubCat(@Param('id', ParseIntPipe) id: number) {
|
@Delete('contract/sub-categories/:id')
|
||||||
|
@ApiOperation({ summary: 'Delete Contract Sub-Category' })
|
||||||
|
@RequirePermission('master_data.drawing_category.manage')
|
||||||
|
deleteContractSubCat(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.masterDataService.deleteContractSubCat(id);
|
return this.masterDataService.deleteContractSubCat(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,11 +146,11 @@ export class DrawingMasterDataController {
|
|||||||
|
|
||||||
@Get('contract/mappings')
|
@Get('contract/mappings')
|
||||||
@ApiOperation({ summary: 'List Contract Drawing Mappings' })
|
@ApiOperation({ summary: 'List Contract Drawing Mappings' })
|
||||||
@ApiQuery({ name: 'projectId', required: true, type: Number })
|
@ApiQuery({ name: 'projectId', required: true, type: String })
|
||||||
@ApiQuery({ name: 'categoryId', required: false, type: Number })
|
@ApiQuery({ name: 'categoryId', required: false, type: Number })
|
||||||
@RequirePermission('document.view')
|
@RequirePermission('document.view')
|
||||||
getContractMappings(
|
getContractMappings(
|
||||||
@Query('projectId', ParseIntPipe) projectId: number,
|
@Query('projectId') projectId: string | number,
|
||||||
@Query('categoryId') categoryId?: number
|
@Query('categoryId') categoryId?: number
|
||||||
) {
|
) {
|
||||||
return this.masterDataService.findContractMappings(
|
return this.masterDataService.findContractMappings(
|
||||||
@@ -217,14 +162,7 @@ export class DrawingMasterDataController {
|
|||||||
@Post('contract/mappings')
|
@Post('contract/mappings')
|
||||||
@ApiOperation({ summary: 'Create Contract Drawing Mapping' })
|
@ApiOperation({ summary: 'Create Contract Drawing Mapping' })
|
||||||
@RequirePermission('master_data.drawing_category.manage')
|
@RequirePermission('master_data.drawing_category.manage')
|
||||||
createContractMapping(
|
createContractMapping(@Body() body: any) {
|
||||||
@Body()
|
|
||||||
body: {
|
|
||||||
projectId: number;
|
|
||||||
categoryId: number;
|
|
||||||
subCategoryId: number;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return this.masterDataService.createContractMapping(body);
|
return this.masterDataService.createContractMapping(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,31 +179,16 @@ export class DrawingMasterDataController {
|
|||||||
|
|
||||||
@Get('shop/main-categories')
|
@Get('shop/main-categories')
|
||||||
@ApiOperation({ summary: 'List Shop Drawing Main Categories' })
|
@ApiOperation({ summary: 'List Shop Drawing Main Categories' })
|
||||||
@ApiQuery({ name: 'projectId', required: true, type: Number })
|
@ApiQuery({ name: 'projectId', required: true, type: String })
|
||||||
@RequirePermission('document.view')
|
@RequirePermission('document.view')
|
||||||
getShopMainCats(@Query('projectId', ParseIntPipe) projectId: number) {
|
getShopMainCats(@Query('projectId') projectId: string | number) {
|
||||||
this.logger.log(
|
|
||||||
`Fetching Shop Main Categories for Project ID: ${projectId}`
|
|
||||||
);
|
|
||||||
return this.masterDataService.findAllShopMainCats(projectId);
|
return this.masterDataService.findAllShopMainCats(projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... (Create/Update/Delete methods remain unchanged) ...
|
|
||||||
|
|
||||||
@Post('shop/main-categories')
|
@Post('shop/main-categories')
|
||||||
@ApiOperation({ summary: 'Create Shop Main Category' })
|
@ApiOperation({ summary: 'Create Shop Main Category' })
|
||||||
@RequirePermission('master_data.drawing_category.manage')
|
@RequirePermission('master_data.drawing_category.manage')
|
||||||
createShopMainCat(
|
createShopMainCat(@Body() body: any) {
|
||||||
@Body()
|
|
||||||
body: {
|
|
||||||
projectId: number;
|
|
||||||
mainCategoryCode: string;
|
|
||||||
mainCategoryName: string;
|
|
||||||
description?: string;
|
|
||||||
isActive?: boolean;
|
|
||||||
sortOrder: number;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return this.masterDataService.createShopMainCat(body);
|
return this.masterDataService.createShopMainCat(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,14 +197,7 @@ export class DrawingMasterDataController {
|
|||||||
@RequirePermission('master_data.drawing_category.manage')
|
@RequirePermission('master_data.drawing_category.manage')
|
||||||
updateShopMainCat(
|
updateShopMainCat(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Body()
|
@Body() body: any
|
||||||
body: {
|
|
||||||
mainCategoryCode?: string;
|
|
||||||
mainCategoryName?: string;
|
|
||||||
description?: string;
|
|
||||||
isActive?: boolean;
|
|
||||||
sortOrder?: number;
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
return this.masterDataService.updateShopMainCat(id, body);
|
return this.masterDataService.updateShopMainCat(id, body);
|
||||||
}
|
}
|
||||||
@@ -299,16 +215,13 @@ export class DrawingMasterDataController {
|
|||||||
|
|
||||||
@Get('shop/sub-categories')
|
@Get('shop/sub-categories')
|
||||||
@ApiOperation({ summary: 'List Shop Drawing Sub-Categories' })
|
@ApiOperation({ summary: 'List Shop Drawing Sub-Categories' })
|
||||||
@ApiQuery({ name: 'projectId', required: true, type: Number })
|
@ApiQuery({ name: 'projectId', required: true, type: String })
|
||||||
@ApiQuery({ name: 'mainCategoryId', required: false, type: Number })
|
@ApiQuery({ name: 'mainCategoryId', required: false, type: Number })
|
||||||
@RequirePermission('document.view')
|
@RequirePermission('document.view')
|
||||||
getShopSubCats(
|
getShopSubCats(
|
||||||
@Query('projectId', ParseIntPipe) projectId: number,
|
@Query('projectId') projectId: string | number,
|
||||||
@Query('mainCategoryId') mainCategoryId?: number
|
@Query('mainCategoryId') mainCategoryId?: number
|
||||||
) {
|
) {
|
||||||
this.logger.log(
|
|
||||||
`Fetching Shop Sub-Categories for Project ID: ${projectId}, MainCategory: ${mainCategoryId}`
|
|
||||||
);
|
|
||||||
return this.masterDataService.findAllShopSubCats(
|
return this.masterDataService.findAllShopSubCats(
|
||||||
projectId,
|
projectId,
|
||||||
mainCategoryId ? Number(mainCategoryId) : undefined
|
mainCategoryId ? Number(mainCategoryId) : undefined
|
||||||
@@ -318,17 +231,7 @@ export class DrawingMasterDataController {
|
|||||||
@Post('shop/sub-categories')
|
@Post('shop/sub-categories')
|
||||||
@ApiOperation({ summary: 'Create Shop Sub-Category' })
|
@ApiOperation({ summary: 'Create Shop Sub-Category' })
|
||||||
@RequirePermission('master_data.drawing_category.manage')
|
@RequirePermission('master_data.drawing_category.manage')
|
||||||
createShopSubCat(
|
createShopSubCat(@Body() body: any) {
|
||||||
@Body()
|
|
||||||
body: {
|
|
||||||
projectId: number;
|
|
||||||
subCategoryCode: string;
|
|
||||||
subCategoryName: string;
|
|
||||||
description?: string;
|
|
||||||
isActive?: boolean;
|
|
||||||
sortOrder: number;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return this.masterDataService.createShopSubCat(body);
|
return this.masterDataService.createShopSubCat(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,14 +240,7 @@ export class DrawingMasterDataController {
|
|||||||
@RequirePermission('master_data.drawing_category.manage')
|
@RequirePermission('master_data.drawing_category.manage')
|
||||||
updateShopSubCat(
|
updateShopSubCat(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Body()
|
@Body() body: any
|
||||||
body: {
|
|
||||||
subCategoryCode?: string;
|
|
||||||
subCategoryName?: string;
|
|
||||||
description?: string;
|
|
||||||
isActive?: boolean;
|
|
||||||
sortOrder?: number;
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
return this.masterDataService.updateShopSubCat(id, body);
|
return this.masterDataService.updateShopSubCat(id, body);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository, InjectEntityManager } from '@nestjs/typeorm';
|
||||||
import { Repository, FindOptionsWhere } from 'typeorm';
|
import { Repository, FindOptionsWhere, EntityManager } from 'typeorm';
|
||||||
|
|
||||||
// Entities
|
// Entities
|
||||||
import { ContractDrawingVolume } from './entities/contract-drawing-volume.entity';
|
import { ContractDrawingVolume } from './entities/contract-drawing-volume.entity';
|
||||||
@@ -9,6 +9,7 @@ import { ContractDrawingSubCategory } from './entities/contract-drawing-sub-cate
|
|||||||
import { ShopDrawingMainCategory } from './entities/shop-drawing-main-category.entity';
|
import { ShopDrawingMainCategory } from './entities/shop-drawing-main-category.entity';
|
||||||
import { ShopDrawingSubCategory } from './entities/shop-drawing-sub-category.entity';
|
import { ShopDrawingSubCategory } from './entities/shop-drawing-sub-category.entity';
|
||||||
import { ContractDrawingSubcatCatMap } from './entities/contract-drawing-subcat-cat-map.entity';
|
import { ContractDrawingSubcatCatMap } from './entities/contract-drawing-subcat-cat-map.entity';
|
||||||
|
import { Project } from '../project/entities/project.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DrawingMasterDataService {
|
export class DrawingMasterDataService {
|
||||||
@@ -24,22 +25,47 @@ export class DrawingMasterDataService {
|
|||||||
@InjectRepository(ShopDrawingSubCategory)
|
@InjectRepository(ShopDrawingSubCategory)
|
||||||
private sdSubCatRepo: Repository<ShopDrawingSubCategory>,
|
private sdSubCatRepo: Repository<ShopDrawingSubCategory>,
|
||||||
@InjectRepository(ContractDrawingSubcatCatMap)
|
@InjectRepository(ContractDrawingSubcatCatMap)
|
||||||
private cdMapRepo: Repository<ContractDrawingSubcatCatMap>
|
private cdMapRepo: Repository<ContractDrawingSubcatCatMap>,
|
||||||
|
@InjectEntityManager()
|
||||||
|
private entityManager: EntityManager
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to resolve projectId (ID or UUID) to internal INT ID
|
||||||
|
*/
|
||||||
|
async resolveProjectId(projectId: number | string): Promise<number> {
|
||||||
|
if (typeof projectId === 'number') return projectId;
|
||||||
|
const num = Number(projectId);
|
||||||
|
if (!isNaN(num)) return num;
|
||||||
|
|
||||||
|
// If it's a string and not a number, it's a UUID (ADR-019)
|
||||||
|
const project = await this.entityManager.findOne(Project, {
|
||||||
|
where: { uuid: projectId as string },
|
||||||
|
select: ['id'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw new NotFoundException(`Project with UUID ${projectId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return project.id;
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// Contract Drawing Volumes
|
// Contract Drawing Volumes
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|
||||||
async findAllVolumes(projectId: number) {
|
async findAllVolumes(projectId: number | string) {
|
||||||
|
const internalId = await this.resolveProjectId(projectId);
|
||||||
return this.cdVolumeRepo.find({
|
return this.cdVolumeRepo.find({
|
||||||
where: { projectId },
|
where: { projectId: internalId },
|
||||||
order: { sortOrder: 'ASC' },
|
order: { sortOrder: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createVolume(data: Partial<ContractDrawingVolume>) {
|
async createVolume(data: any) {
|
||||||
const volume = this.cdVolumeRepo.create(data);
|
const internalId = await this.resolveProjectId(data.projectId);
|
||||||
|
const volume = this.cdVolumeRepo.create({ ...data, projectId: internalId });
|
||||||
return this.cdVolumeRepo.save(volume);
|
return this.cdVolumeRepo.save(volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,15 +87,17 @@ export class DrawingMasterDataService {
|
|||||||
// Contract Drawing Categories
|
// Contract Drawing Categories
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|
||||||
async findAllCategories(projectId: number) {
|
async findAllCategories(projectId: number | string) {
|
||||||
|
const internalId = await this.resolveProjectId(projectId);
|
||||||
return this.cdCatRepo.find({
|
return this.cdCatRepo.find({
|
||||||
where: { projectId },
|
where: { projectId: internalId },
|
||||||
order: { sortOrder: 'ASC' },
|
order: { sortOrder: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createCategory(data: Partial<ContractDrawingCategory>) {
|
async createCategory(data: any) {
|
||||||
const cat = this.cdCatRepo.create(data);
|
const internalId = await this.resolveProjectId(data.projectId);
|
||||||
|
const cat = this.cdCatRepo.create({ ...data, projectId: internalId });
|
||||||
return this.cdCatRepo.save(cat);
|
return this.cdCatRepo.save(cat);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,15 +119,17 @@ export class DrawingMasterDataService {
|
|||||||
// Contract Drawing Sub-Categories
|
// Contract Drawing Sub-Categories
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|
||||||
async findAllContractSubCats(projectId: number) {
|
async findAllContractSubCats(projectId: number | string) {
|
||||||
|
const internalId = await this.resolveProjectId(projectId);
|
||||||
return this.cdSubCatRepo.find({
|
return this.cdSubCatRepo.find({
|
||||||
where: { projectId },
|
where: { projectId: internalId },
|
||||||
order: { sortOrder: 'ASC' },
|
order: { sortOrder: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createContractSubCat(data: Partial<ContractDrawingSubCategory>) {
|
async createContractSubCat(data: any) {
|
||||||
const subCat = this.cdSubCatRepo.create(data);
|
const internalId = await this.resolveProjectId(data.projectId);
|
||||||
|
const subCat = this.cdSubCatRepo.create({ ...data, projectId: internalId });
|
||||||
return this.cdSubCatRepo.save(subCat);
|
return this.cdSubCatRepo.save(subCat);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,8 +154,9 @@ export class DrawingMasterDataService {
|
|||||||
// Contract Drawing Mappings (Category <-> Sub-Category)
|
// Contract Drawing Mappings (Category <-> Sub-Category)
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|
||||||
async findContractMappings(projectId: number, categoryId?: number) {
|
async findContractMappings(projectId: number | string, categoryId?: number) {
|
||||||
const where: FindOptionsWhere<ContractDrawingSubcatCatMap> = { projectId };
|
const internalId = await this.resolveProjectId(projectId);
|
||||||
|
const where: FindOptionsWhere<ContractDrawingSubcatCatMap> = { projectId: internalId };
|
||||||
if (categoryId) {
|
if (categoryId) {
|
||||||
where.categoryId = categoryId;
|
where.categoryId = categoryId;
|
||||||
}
|
}
|
||||||
@@ -136,15 +167,12 @@ export class DrawingMasterDataService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createContractMapping(data: {
|
async createContractMapping(data: any) {
|
||||||
projectId: number;
|
const internalId = await this.resolveProjectId(data.projectId);
|
||||||
categoryId: number;
|
|
||||||
subCategoryId: number;
|
|
||||||
}) {
|
|
||||||
// Check if mapping already exists to prevent duplicates (though DB has UNIQUE constraint)
|
// Check if mapping already exists to prevent duplicates (though DB has UNIQUE constraint)
|
||||||
const existing = await this.cdMapRepo.findOne({
|
const existing = await this.cdMapRepo.findOne({
|
||||||
where: {
|
where: {
|
||||||
projectId: data.projectId,
|
projectId: internalId,
|
||||||
categoryId: data.categoryId,
|
categoryId: data.categoryId,
|
||||||
subCategoryId: data.subCategoryId,
|
subCategoryId: data.subCategoryId,
|
||||||
},
|
},
|
||||||
@@ -152,7 +180,7 @@ export class DrawingMasterDataService {
|
|||||||
|
|
||||||
if (existing) return existing;
|
if (existing) return existing;
|
||||||
|
|
||||||
const map = this.cdMapRepo.create(data);
|
const map = this.cdMapRepo.create({ ...data, projectId: internalId });
|
||||||
return this.cdMapRepo.save(map);
|
return this.cdMapRepo.save(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,15 +195,17 @@ export class DrawingMasterDataService {
|
|||||||
// Shop Drawing Main Categories
|
// Shop Drawing Main Categories
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|
||||||
async findAllShopMainCats(projectId: number) {
|
async findAllShopMainCats(projectId: number | string) {
|
||||||
|
const internalId = await this.resolveProjectId(projectId);
|
||||||
return this.sdMainCatRepo.find({
|
return this.sdMainCatRepo.find({
|
||||||
where: { projectId },
|
where: { projectId: internalId },
|
||||||
order: { sortOrder: 'ASC' },
|
order: { sortOrder: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createShopMainCat(data: Partial<ShopDrawingMainCategory>) {
|
async createShopMainCat(data: any) {
|
||||||
const cat = this.sdMainCatRepo.create(data);
|
const internalId = await this.resolveProjectId(data.projectId);
|
||||||
|
const cat = this.sdMainCatRepo.create({ ...data, projectId: internalId });
|
||||||
return this.sdMainCatRepo.save(cat);
|
return this.sdMainCatRepo.save(cat);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,9 +227,10 @@ export class DrawingMasterDataService {
|
|||||||
// Shop Drawing Sub-Categories
|
// Shop Drawing Sub-Categories
|
||||||
// =====================================================
|
// =====================================================
|
||||||
|
|
||||||
async findAllShopSubCats(projectId: number, mainCategoryId?: number) {
|
async findAllShopSubCats(projectId: number | string, mainCategoryId?: number) {
|
||||||
|
const internalId = await this.resolveProjectId(projectId);
|
||||||
const where: FindOptionsWhere<ShopDrawingSubCategory> = {
|
const where: FindOptionsWhere<ShopDrawingSubCategory> = {
|
||||||
projectId,
|
projectId: internalId,
|
||||||
...(mainCategoryId ? { mainCategoryId } : {}),
|
...(mainCategoryId ? { mainCategoryId } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -209,8 +240,9 @@ export class DrawingMasterDataService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createShopSubCat(data: Partial<ShopDrawingSubCategory>) {
|
async createShopSubCat(data: any) {
|
||||||
const subCat = this.sdSubCatRepo.create(data);
|
const internalId = await this.resolveProjectId(data.projectId);
|
||||||
|
const subCat = this.sdSubCatRepo.create({ ...data, projectId: internalId });
|
||||||
return this.sdSubCatRepo.save(subCat);
|
return this.sdSubCatRepo.save(subCat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,151 +13,78 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
|
||||||
import { MasterService } from './master.service';
|
import { MasterService } from './master.service';
|
||||||
|
|
||||||
// DTOs (สมมติว่ามีการสร้างไฟล์เหล่านี้แล้วตามแผนงาน)
|
|
||||||
import { CreateTagDto } from './dto/create-tag.dto';
|
|
||||||
import { UpdateTagDto } from './dto/update-tag.dto';
|
|
||||||
import { SearchTagDto } from './dto/search-tag.dto';
|
|
||||||
import { CreateDisciplineDto } from './dto/create-discipline.dto'; // [New]
|
|
||||||
import { CreateSubTypeDto } from './dto/create-sub-type.dto'; // [New]
|
|
||||||
import { SaveNumberFormatDto } from './dto/save-number-format.dto'; // [New]
|
|
||||||
|
|
||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
|
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||||
|
|
||||||
|
// Import DTOs
|
||||||
|
import { CreateTagDto } from './dto/create-tag.dto';
|
||||||
|
import { UpdateTagDto } from './dto/update-tag.dto';
|
||||||
|
import { SearchTagDto } from './dto/search-tag.dto';
|
||||||
|
|
||||||
@ApiTags('Master Data')
|
@ApiTags('Master Data')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||||
@Controller('master')
|
@Controller('master')
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
export class MasterController {
|
export class MasterController {
|
||||||
constructor(private readonly masterService: MasterService) {}
|
constructor(private readonly masterService: MasterService) {}
|
||||||
|
|
||||||
// =================================================================
|
// --- Correspondence Types ---
|
||||||
// 📦 Common Dropdowns (Read-Only)
|
|
||||||
// =================================================================
|
|
||||||
|
|
||||||
@Get('correspondence-types')
|
@Get('correspondence-types')
|
||||||
@ApiOperation({ summary: 'Get all active correspondence types' })
|
@ApiOperation({ summary: 'Get all correspondence types' })
|
||||||
getCorrespondenceTypes() {
|
findAllCorrespondenceTypes() {
|
||||||
return this.masterService.findAllCorrespondenceTypes();
|
return this.masterService.findAllCorrespondenceTypes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('correspondence-types')
|
@Post('correspondence-types')
|
||||||
@RequirePermission('master_data.manage')
|
@RequirePermission('master_data.manage')
|
||||||
@ApiOperation({ summary: 'Create Correspondence Type' })
|
|
||||||
createCorrespondenceType(@Body() dto: any) {
|
createCorrespondenceType(@Body() dto: any) {
|
||||||
return this.masterService.createCorrespondenceType(dto);
|
return this.masterService.createCorrespondenceType(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('correspondence-types/:id')
|
// --- RFA Types ---
|
||||||
@RequirePermission('master_data.manage')
|
|
||||||
@ApiOperation({ summary: 'Update Correspondence Type' })
|
|
||||||
updateCorrespondenceType(
|
|
||||||
@Param('id', ParseIntPipe) id: number,
|
|
||||||
@Body() dto: any
|
|
||||||
) {
|
|
||||||
return this.masterService.updateCorrespondenceType(id, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete('correspondence-types/:id')
|
|
||||||
@RequirePermission('master_data.manage')
|
|
||||||
@ApiOperation({ summary: 'Delete Correspondence Type' })
|
|
||||||
deleteCorrespondenceType(@Param('id', ParseIntPipe) id: number) {
|
|
||||||
return this.masterService.deleteCorrespondenceType(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('correspondence-statuses')
|
|
||||||
@ApiOperation({ summary: 'Get all active correspondence statuses' })
|
|
||||||
getCorrespondenceStatuses() {
|
|
||||||
return this.masterService.findAllCorrespondenceStatuses();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('rfa-types')
|
@Get('rfa-types')
|
||||||
@ApiOperation({ summary: 'Get all active RFA types' })
|
@ApiOperation({ summary: 'Get all RFA types' })
|
||||||
@ApiQuery({ name: 'contractId', required: false, type: Number })
|
@ApiQuery({ name: 'contractId', required: false, type: String })
|
||||||
getRfaTypes(@Query('contractId') contractId?: number) {
|
findAllRfaTypes(@Query('contractId') contractId?: string | number) {
|
||||||
return this.masterService.findAllRfaTypes(contractId);
|
return this.masterService.findAllRfaTypes(contractId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('rfa-types')
|
@Post('rfa-types')
|
||||||
@RequirePermission('master_data.manage')
|
@RequirePermission('master_data.manage')
|
||||||
@ApiOperation({ summary: 'Create RFA Type' })
|
|
||||||
createRfaType(@Body() dto: any) {
|
createRfaType(@Body() dto: any) {
|
||||||
// Note: Should use proper DTO. Delegating to service.
|
|
||||||
// Need to add createRfaType to MasterService or RfaService?
|
|
||||||
// Given the context, MasterService seems appropriate for "Reference Data".
|
|
||||||
return this.masterService.createRfaType(dto);
|
return this.masterService.createRfaType(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('rfa-types/:id')
|
// --- Disciplines ---
|
||||||
@RequirePermission('master_data.manage')
|
|
||||||
@ApiOperation({ summary: 'Update RFA Type' })
|
|
||||||
updateRfaType(@Param('id', ParseIntPipe) id: number, @Body() dto: any) {
|
|
||||||
return this.masterService.updateRfaType(id, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete('rfa-types/:id')
|
|
||||||
@RequirePermission('master_data.manage')
|
|
||||||
@ApiOperation({ summary: 'Delete RFA Type' })
|
|
||||||
deleteRfaType(@Param('id', ParseIntPipe) id: number) {
|
|
||||||
return this.masterService.deleteRfaType(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('rfa-statuses')
|
|
||||||
@ApiOperation({ summary: 'Get all active RFA status codes' })
|
|
||||||
getRfaStatuses() {
|
|
||||||
return this.masterService.findAllRfaStatuses();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('rfa-approve-codes')
|
|
||||||
@ApiOperation({ summary: 'Get all active RFA approve codes' })
|
|
||||||
getRfaApproveCodes() {
|
|
||||||
return this.masterService.findAllRfaApproveCodes();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('circulation-statuses')
|
|
||||||
@ApiOperation({ summary: 'Get all active circulation statuses' })
|
|
||||||
getCirculationStatuses() {
|
|
||||||
return this.masterService.findAllCirculationStatuses();
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// 🏗️ Disciplines Management (Req 6B)
|
|
||||||
// =================================================================
|
|
||||||
|
|
||||||
@Get('disciplines')
|
@Get('disciplines')
|
||||||
@ApiOperation({ summary: 'Get disciplines (filter by contract optional)' })
|
@ApiOperation({ summary: 'Get all disciplines' })
|
||||||
@ApiQuery({ name: 'contractId', required: false, type: Number })
|
@ApiQuery({ name: 'contractId', required: false, type: String })
|
||||||
getDisciplines(@Query('contractId') contractId?: number) {
|
findAllDisciplines(@Query('contractId') contractId?: string | number) {
|
||||||
return this.masterService.findAllDisciplines(contractId);
|
return this.masterService.findAllDisciplines(contractId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('disciplines')
|
@Post('disciplines')
|
||||||
@RequirePermission('master_data.manage') // สิทธิ์ Admin
|
@RequirePermission('master_data.manage')
|
||||||
@ApiOperation({ summary: 'Create a new discipline' })
|
createDiscipline(@Body() dto: any) {
|
||||||
createDiscipline(@Body() dto: CreateDisciplineDto) {
|
|
||||||
return this.masterService.createDiscipline(dto);
|
return this.masterService.createDiscipline(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete('disciplines/:id')
|
@Delete('disciplines/:id')
|
||||||
@RequirePermission('master_data.manage')
|
@RequirePermission('master_data.manage')
|
||||||
@ApiOperation({ summary: 'Delete a discipline' })
|
|
||||||
deleteDiscipline(@Param('id', ParseIntPipe) id: number) {
|
deleteDiscipline(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.masterService.deleteDiscipline(id);
|
return this.masterService.deleteDiscipline(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =================================================================
|
// --- Sub Types ---
|
||||||
// 📑 Correspondence Sub-Types (Req 6B)
|
|
||||||
// =================================================================
|
|
||||||
|
|
||||||
@Get('sub-types')
|
@Get('sub-types')
|
||||||
@ApiOperation({ summary: 'Get sub-types (filter by contract/type optional)' })
|
@ApiOperation({ summary: 'Get all sub-types' })
|
||||||
@ApiQuery({ name: 'contractId', required: false, type: Number })
|
@ApiQuery({ name: 'contractId', required: false, type: String })
|
||||||
@ApiQuery({ name: 'typeId', required: false, type: Number })
|
findAllSubTypes(
|
||||||
getSubTypes(
|
@Query('contractId') contractId?: string | number,
|
||||||
@Query('contractId') contractId?: number,
|
|
||||||
@Query('typeId') typeId?: number
|
@Query('typeId') typeId?: number
|
||||||
) {
|
) {
|
||||||
return this.masterService.findAllSubTypes(contractId, typeId);
|
return this.masterService.findAllSubTypes(contractId, typeId);
|
||||||
@@ -165,62 +92,43 @@ export class MasterController {
|
|||||||
|
|
||||||
@Post('sub-types')
|
@Post('sub-types')
|
||||||
@RequirePermission('master_data.manage')
|
@RequirePermission('master_data.manage')
|
||||||
@ApiOperation({ summary: 'Create/Map a new sub-type' })
|
createSubType(@Body() dto: any) {
|
||||||
createSubType(@Body() dto: CreateSubTypeDto) {
|
|
||||||
return this.masterService.createSubType(dto);
|
return this.masterService.createSubType(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete('sub-types/:id')
|
// --- Numbering Formats ---
|
||||||
@RequirePermission('master_data.manage')
|
|
||||||
@ApiOperation({ summary: 'Delete a sub-type' })
|
|
||||||
deleteSubType(@Param('id', ParseIntPipe) id: number) {
|
|
||||||
return this.masterService.deleteSubType(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// 🔢 Numbering Formats (Admin Config)
|
|
||||||
// =================================================================
|
|
||||||
|
|
||||||
@Get('numbering-formats')
|
@Get('numbering-formats')
|
||||||
@RequirePermission('master_data.manage') // ข้อมูล config ควรสงวนสิทธิ์
|
@ApiOperation({ summary: 'Get numbering format for project/type' })
|
||||||
@ApiOperation({ summary: 'Get numbering format for specific project/type' })
|
findNumberFormat(
|
||||||
getNumberFormat(
|
@Query('projectId') projectId: string | number,
|
||||||
@Query('projectId', ParseIntPipe) projectId: number,
|
|
||||||
@Query('typeId', ParseIntPipe) typeId: number
|
@Query('typeId', ParseIntPipe) typeId: number
|
||||||
) {
|
) {
|
||||||
return this.masterService.findNumberFormat(projectId, typeId);
|
return this.masterService.findNumberFormat(projectId, typeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('numbering-formats')
|
@Post('numbering-formats')
|
||||||
@RequirePermission('system.manage_all') // เฉพาะ Superadmin/System Admin
|
@RequirePermission('master_data.manage')
|
||||||
@ApiOperation({ summary: 'Save or Update numbering format template' })
|
saveNumberFormat(@Body() dto: any) {
|
||||||
saveNumberFormat(@Body() dto: SaveNumberFormatDto) {
|
|
||||||
return this.masterService.saveNumberFormat(dto);
|
return this.masterService.saveNumberFormat(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =================================================================
|
// --- Tags ---
|
||||||
// 🏷️ Tag Management
|
|
||||||
// =================================================================
|
|
||||||
|
|
||||||
@Get('tags')
|
@Get('tags')
|
||||||
@ApiOperation({ summary: 'Get all tags (supports search & pagination)' })
|
@ApiOperation({ summary: 'Get all tags' })
|
||||||
getTags(@Query() query: SearchTagDto) {
|
findAllTags(@Query() query: SearchTagDto) {
|
||||||
return this.masterService.findAllTags(query);
|
return this.masterService.findAllTags(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('tags/:id')
|
@Get('tags/:id')
|
||||||
@ApiOperation({ summary: 'Get a tag by ID' })
|
@ApiOperation({ summary: 'Get a tag by ID' })
|
||||||
getTagById(@Param('id', ParseIntPipe) id: number) {
|
findOneTag(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.masterService.findOneTag(id);
|
return this.masterService.findOneTag(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('tags')
|
@Post('tags')
|
||||||
@RequirePermission('master_data.tag.manage')
|
@RequirePermission('master_data.tag.manage')
|
||||||
@ApiOperation({ summary: 'Create a new tag' })
|
@ApiOperation({ summary: 'Create a new tag' })
|
||||||
createTag(
|
createTag(@Body() dto: CreateTagDto, @CurrentUser() user: any) {
|
||||||
@CurrentUser() user: { userId: number },
|
|
||||||
@Body() dto: CreateTagDto
|
|
||||||
) {
|
|
||||||
return this.masterService.createTag(dto, user.userId);
|
return this.masterService.createTag(dto, user.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
ConflictException,
|
ConflictException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository, InjectEntityManager } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository, EntityManager } from 'typeorm';
|
||||||
|
|
||||||
// Import Entities
|
// Import Entities
|
||||||
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
|
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
|
||||||
@@ -21,6 +21,8 @@ import { Tag } from './entities/tag.entity';
|
|||||||
import { Discipline } from './entities/discipline.entity';
|
import { Discipline } from './entities/discipline.entity';
|
||||||
import { CorrespondenceSubType } from '../correspondence/entities/correspondence-sub-type.entity';
|
import { CorrespondenceSubType } from '../correspondence/entities/correspondence-sub-type.entity';
|
||||||
import { DocumentNumberFormat } from '../document-numbering/entities/document-number-format.entity';
|
import { DocumentNumberFormat } from '../document-numbering/entities/document-number-format.entity';
|
||||||
|
import { Project } from '../project/entities/project.entity';
|
||||||
|
import { Contract } from '../contract/entities/contract.entity';
|
||||||
|
|
||||||
// Import DTOs
|
// Import DTOs
|
||||||
import { CreateTagDto } from './dto/create-tag.dto';
|
import { CreateTagDto } from './dto/create-tag.dto';
|
||||||
@@ -54,12 +56,41 @@ export class MasterService {
|
|||||||
@InjectRepository(CorrespondenceSubType)
|
@InjectRepository(CorrespondenceSubType)
|
||||||
private readonly subTypeRepo: Repository<CorrespondenceSubType>,
|
private readonly subTypeRepo: Repository<CorrespondenceSubType>,
|
||||||
@InjectRepository(DocumentNumberFormat)
|
@InjectRepository(DocumentNumberFormat)
|
||||||
private readonly formatRepo: Repository<DocumentNumberFormat>
|
private readonly formatRepo: Repository<DocumentNumberFormat>,
|
||||||
|
|
||||||
|
@InjectEntityManager()
|
||||||
|
private readonly entityManager: EntityManager
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// ... (Method เดิม: findAllCorrespondenceTypes, findAllCorrespondenceStatuses, ฯลฯ เก็บไว้เหมือนเดิม) ...
|
/**
|
||||||
// หมายเหตุ: ตรวจสอบว่า Entity ใช้ชื่อ property ว่า isActive หรือ is_active (ใน SQL เป็น is_active แต่ใน Entity มักเป็น isActive)
|
* Helper to resolve projectId (ID or UUID) to internal INT ID
|
||||||
// โค้ดเดิมใช้ `where: { isActive: true }` ซึ่งถูกต้องถ้า Entity map column name แล้ว
|
*/
|
||||||
|
async resolveProjectId(projectId: number | string): Promise<number> {
|
||||||
|
if (typeof projectId === 'number') return projectId;
|
||||||
|
const num = Number(projectId);
|
||||||
|
if (!isNaN(num)) return num;
|
||||||
|
const project = await this.entityManager.findOne(Project, {
|
||||||
|
where: { uuid: projectId as string },
|
||||||
|
select: ['id'],
|
||||||
|
});
|
||||||
|
if (!project) throw new NotFoundException(`Project with UUID ${projectId} not found`);
|
||||||
|
return project.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to resolve contractId (ID or UUID) to internal INT ID
|
||||||
|
*/
|
||||||
|
async resolveContractId(contractId: number | string): Promise<number> {
|
||||||
|
if (typeof contractId === 'number') return contractId;
|
||||||
|
const num = Number(contractId);
|
||||||
|
if (!isNaN(num)) return num;
|
||||||
|
const contract = await this.entityManager.findOne(Contract, {
|
||||||
|
where: { uuid: contractId as string },
|
||||||
|
select: ['id'],
|
||||||
|
});
|
||||||
|
if (!contract) throw new NotFoundException(`Contract with UUID ${contractId} not found`);
|
||||||
|
return contract.id;
|
||||||
|
}
|
||||||
|
|
||||||
async findAllCorrespondenceTypes() {
|
async findAllCorrespondenceTypes() {
|
||||||
return this.corrTypeRepo.find({
|
return this.corrTypeRepo.find({
|
||||||
@@ -92,27 +123,29 @@ export class MasterService {
|
|||||||
order: { sortOrder: 'ASC' },
|
order: { sortOrder: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async findAllRfaTypes(contractId?: number) {
|
async findAllRfaTypes(contractId?: number | string) {
|
||||||
const where: any = { isActive: true };
|
const where: any = { isActive: true };
|
||||||
if (contractId) {
|
if (contractId) {
|
||||||
where.contractId = contractId;
|
where.contractId = await this.resolveContractId(contractId);
|
||||||
}
|
}
|
||||||
return this.rfaTypeRepo.find({
|
return this.rfaTypeRepo.find({
|
||||||
where,
|
where,
|
||||||
order: { typeCode: 'ASC' },
|
order: { typeCode: 'ASC' },
|
||||||
relations: contractId ? [] : [], // Add relations if needed later
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createRfaType(dto: any) {
|
async createRfaType(dto: any) {
|
||||||
// Validate unique code if needed
|
const internalContractId = await this.resolveContractId(dto.contractId);
|
||||||
const rfaType = this.rfaTypeRepo.create(dto);
|
const rfaType = this.rfaTypeRepo.create({ ...dto, contractId: internalContractId });
|
||||||
return this.rfaTypeRepo.save(rfaType);
|
return this.rfaTypeRepo.save(rfaType);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateRfaType(id: number, dto: any) {
|
async updateRfaType(id: number, dto: any) {
|
||||||
const rfaType = await this.rfaTypeRepo.findOne({ where: { id } });
|
const rfaType = await this.rfaTypeRepo.findOne({ where: { id } });
|
||||||
if (!rfaType) throw new NotFoundException('RFA Type not found');
|
if (!rfaType) throw new NotFoundException('RFA Type not found');
|
||||||
|
if (dto.contractId) {
|
||||||
|
dto.contractId = await this.resolveContractId(dto.contractId);
|
||||||
|
}
|
||||||
Object.assign(rfaType, dto);
|
Object.assign(rfaType, dto);
|
||||||
return this.rfaTypeRepo.save(rfaType);
|
return this.rfaTypeRepo.save(rfaType);
|
||||||
}
|
}
|
||||||
@@ -146,31 +179,32 @@ export class MasterService {
|
|||||||
// 🏗️ Disciplines Logic
|
// 🏗️ Disciplines Logic
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
||||||
async findAllDisciplines(contractId?: number) {
|
async findAllDisciplines(contractId?: number | string) {
|
||||||
const query = this.disciplineRepo
|
const query = this.disciplineRepo
|
||||||
.createQueryBuilder('d')
|
.createQueryBuilder('d')
|
||||||
.leftJoinAndSelect('d.contract', 'c')
|
.leftJoinAndSelect('d.contract', 'c')
|
||||||
.orderBy('d.disciplineCode', 'ASC');
|
.orderBy('d.disciplineCode', 'ASC');
|
||||||
|
|
||||||
if (contractId) {
|
if (contractId) {
|
||||||
query.where('d.contractId = :contractId', { contractId });
|
const internalId = await this.resolveContractId(contractId);
|
||||||
|
query.where('d.contractId = :contractId', { contractId: internalId });
|
||||||
}
|
}
|
||||||
// เพิ่มเงื่อนไข Active หากต้องการ
|
|
||||||
query.andWhere('d.isActive = :isActive', { isActive: true });
|
query.andWhere('d.isActive = :isActive', { isActive: true });
|
||||||
|
|
||||||
return query.getMany();
|
return query.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
async createDiscipline(dto: CreateDisciplineDto) {
|
async createDiscipline(dto: any) {
|
||||||
|
const internalContractId = await this.resolveContractId(dto.contractId);
|
||||||
const exists = await this.disciplineRepo.findOne({
|
const exists = await this.disciplineRepo.findOne({
|
||||||
where: { contractId: dto.contractId, disciplineCode: dto.disciplineCode },
|
where: { contractId: internalContractId, disciplineCode: dto.disciplineCode },
|
||||||
});
|
});
|
||||||
if (exists)
|
if (exists)
|
||||||
throw new ConflictException(
|
throw new ConflictException(
|
||||||
'Discipline code already exists in this contract'
|
'Discipline code already exists in this contract'
|
||||||
);
|
);
|
||||||
|
|
||||||
const discipline = this.disciplineRepo.create(dto);
|
const discipline = this.disciplineRepo.create({ ...dto, contractId: internalContractId });
|
||||||
return this.disciplineRepo.save(discipline);
|
return this.disciplineRepo.save(discipline);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,23 +219,25 @@ export class MasterService {
|
|||||||
// 📑 Sub-Types Logic
|
// 📑 Sub-Types Logic
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
||||||
async findAllSubTypes(contractId?: number, typeId?: number) {
|
async findAllSubTypes(contractId?: number | string, typeId?: number) {
|
||||||
const query = this.subTypeRepo
|
const query = this.subTypeRepo
|
||||||
.createQueryBuilder('st')
|
.createQueryBuilder('st')
|
||||||
.leftJoinAndSelect('st.contract', 'c')
|
.leftJoinAndSelect('st.contract', 'c')
|
||||||
.leftJoinAndSelect('st.correspondenceType', 'ct')
|
.leftJoinAndSelect('st.correspondenceType', 'ct')
|
||||||
.orderBy('st.subTypeCode', 'ASC');
|
.orderBy('st.subTypeCode', 'ASC');
|
||||||
|
|
||||||
if (contractId)
|
if (contractId) {
|
||||||
query.andWhere('st.contractId = :contractId', { contractId });
|
const internalId = await this.resolveContractId(contractId);
|
||||||
|
query.andWhere('st.contractId = :contractId', { contractId: internalId });
|
||||||
|
}
|
||||||
if (typeId) query.andWhere('st.correspondenceTypeId = :typeId', { typeId });
|
if (typeId) query.andWhere('st.correspondenceTypeId = :typeId', { typeId });
|
||||||
|
|
||||||
return query.getMany();
|
return query.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSubType(dto: CreateSubTypeDto) {
|
async createSubType(dto: any) {
|
||||||
// อาจจะเช็ค Duplicate code ด้วย logic คล้าย discipline
|
const internalContractId = await this.resolveContractId(dto.contractId);
|
||||||
const subType = this.subTypeRepo.create(dto);
|
const subType = this.subTypeRepo.create({ ...dto, contractId: internalContractId });
|
||||||
return this.subTypeRepo.save(subType);
|
return this.subTypeRepo.save(subType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,47 +252,43 @@ export class MasterService {
|
|||||||
// 🔢 Numbering Formats Logic
|
// 🔢 Numbering Formats Logic
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
||||||
async findNumberFormat(projectId: number, typeId: number) {
|
async findNumberFormat(projectId: number | string, typeId: number) {
|
||||||
|
const internalId = await this.resolveProjectId(projectId);
|
||||||
const format = await this.formatRepo.findOne({
|
const format = await this.formatRepo.findOne({
|
||||||
where: { projectId, correspondenceTypeId: typeId },
|
where: { projectId: internalId, correspondenceTypeId: typeId },
|
||||||
});
|
});
|
||||||
if (!format) {
|
return format || null;
|
||||||
// Optional: Return default format structure or null
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return format;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveNumberFormat(dto: SaveNumberFormatDto) {
|
async saveNumberFormat(dto: any) {
|
||||||
// Check if exists (Upsert)
|
const internalProjectId = await this.resolveProjectId(dto.projectId);
|
||||||
let format = await this.formatRepo.findOne({
|
let format = await this.formatRepo.findOne({
|
||||||
where: {
|
where: {
|
||||||
projectId: dto.projectId,
|
projectId: internalProjectId,
|
||||||
correspondenceTypeId: dto.correspondenceTypeId,
|
correspondenceTypeId: dto.correspondenceTypeId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (format) {
|
if (format) {
|
||||||
format.formatTemplate = dto.formatTemplate;
|
format.formatTemplate = dto.formatTemplate;
|
||||||
// format.updatedBy = ... (ถ้ามี)
|
|
||||||
} else {
|
} else {
|
||||||
format = this.formatRepo.create({
|
format = this.formatRepo.create({
|
||||||
projectId: dto.projectId,
|
...dto,
|
||||||
correspondenceTypeId: dto.correspondenceTypeId,
|
projectId: internalProjectId,
|
||||||
formatTemplate: dto.formatTemplate,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.formatRepo.save(format);
|
return this.formatRepo.save(format);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... (Tag Logic เดิม คงไว้ตามปกติ) ...
|
|
||||||
async findAllTags(query?: SearchTagDto) {
|
async findAllTags(query?: SearchTagDto) {
|
||||||
const qb = this.tagRepo.createQueryBuilder('tag');
|
const qb = this.tagRepo.createQueryBuilder('tag');
|
||||||
|
|
||||||
if (query?.project_id) {
|
if (query?.project_id) {
|
||||||
|
// In Tags, we use project_id (INT) directly or resolve if UUID passed via query
|
||||||
|
const internalId = await this.resolveProjectId(query.project_id);
|
||||||
qb.andWhere('tag.project_id = :projectId', {
|
qb.andWhere('tag.project_id = :projectId', {
|
||||||
projectId: query.project_id,
|
projectId: internalId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,16 +320,21 @@ export class MasterService {
|
|||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createTag(dto: CreateTagDto, userId: number) {
|
async createTag(dto: any, userId: number) {
|
||||||
|
const internalProjectId = dto.project_id ? await this.resolveProjectId(dto.project_id) : null;
|
||||||
const tag = this.tagRepo.create({
|
const tag = this.tagRepo.create({
|
||||||
...dto,
|
...dto,
|
||||||
|
project_id: internalProjectId,
|
||||||
created_by: userId,
|
created_by: userId,
|
||||||
});
|
});
|
||||||
return this.tagRepo.save(tag);
|
return this.tagRepo.save(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateTag(id: number, dto: UpdateTagDto) {
|
async updateTag(id: number, dto: any) {
|
||||||
const tag = await this.findOneTag(id);
|
const tag = await this.findOneTag(id);
|
||||||
|
if (dto.project_id) {
|
||||||
|
dto.project_id = await this.resolveProjectId(dto.project_id);
|
||||||
|
}
|
||||||
Object.assign(tag, dto);
|
Object.assign(tag, dto);
|
||||||
return this.tagRepo.save(tag);
|
return this.tagRepo.save(tag);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
BeforeInsert,
|
BeforeInsert,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { v7 as uuidv7 } from 'uuid';
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
import { Exclude } from 'class-transformer';
|
import { Exclude, Expose } from 'class-transformer';
|
||||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||||
import { Contract } from '../../contract/entities/contract.entity';
|
import { Contract } from '../../contract/entities/contract.entity';
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ export class Project extends BaseEntity {
|
|||||||
@Exclude()
|
@Exclude()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
|
@Expose({ name: 'id' })
|
||||||
@Column({
|
@Column({
|
||||||
type: 'uuid',
|
type: 'uuid',
|
||||||
unique: true,
|
unique: true,
|
||||||
|
|||||||
@@ -214,10 +214,12 @@ export default function ContractsPage() {
|
|||||||
|
|
||||||
const handleEdit = (contract: Contract) => {
|
const handleEdit = (contract: Contract) => {
|
||||||
setEditingUuid(contract.uuid);
|
setEditingUuid(contract.uuid);
|
||||||
|
// ADR-019: projectId might be a number or a UUID string from the entity response
|
||||||
|
const pId = String((contract as any).id || (contract as any).projectId || "");
|
||||||
reset({
|
reset({
|
||||||
contractCode: contract.contractCode,
|
contractCode: contract.contractCode,
|
||||||
contractName: contract.contractName,
|
contractName: contract.contractName,
|
||||||
projectId: contract.projectId?.toString() || "",
|
projectId: pId,
|
||||||
description: contract.description || "",
|
description: contract.description || "",
|
||||||
startDate: contract.startDate ? new Date(contract.startDate).toISOString().split('T')[0] : "",
|
startDate: contract.startDate ? new Date(contract.startDate).toISOString().split('T')[0] : "",
|
||||||
endDate: contract.endDate ? new Date(contract.endDate).toISOString().split('T')[0] : "",
|
endDate: contract.endDate ? new Date(contract.endDate).toISOString().split('T')[0] : "",
|
||||||
@@ -239,10 +241,11 @@ export default function ContractsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = (data: ContractFormData) => {
|
const onSubmit = (data: ContractFormData) => {
|
||||||
|
// ADR-019: Resolve projectId (ID or UUID)
|
||||||
const submitData = {
|
const submitData = {
|
||||||
...data,
|
...data,
|
||||||
projectId: parseInt(data.projectId),
|
projectId: isNaN(Number(data.projectId)) ? data.projectId : Number(data.projectId),
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
if (editingUuid) {
|
if (editingUuid) {
|
||||||
updateContract.mutate({ uuid: editingUuid, data: submitData });
|
updateContract.mutate({ uuid: editingUuid, data: submitData });
|
||||||
@@ -304,8 +307,8 @@ export default function ContractsPage() {
|
|||||||
<SelectValue placeholder="Select Project" />
|
<SelectValue placeholder="Select Project" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{(projects as Project[])?.map((p) => (
|
{(projects as any[])?.map((p) => (
|
||||||
<SelectItem key={p.id} value={p.id.toString()}>
|
<SelectItem key={p.uuid || p.id} value={String(p.id || p.uuid)}>
|
||||||
{p.projectCode} - {p.projectName}
|
{p.projectCode} - {p.projectName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ export default function ContractCategoriesPage() {
|
|||||||
)}
|
)}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{projects.map((project: { id: number; projectName: string; projectCode: string }) => (
|
{(projects as any[]).map((project) => (
|
||||||
<SelectItem key={project.id} value={String(project.id)}>
|
<SelectItem key={project.uuid || project.id} value={String(project.id || project.uuid)}>
|
||||||
{project.projectCode} - {project.projectName}
|
{project.projectCode} - {project.projectName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ export default function ContractSubCategoriesPage() {
|
|||||||
)}
|
)}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{projects.map((project: { id: number; projectName: string; projectCode: string }) => (
|
{(projects as any[]).map((project) => (
|
||||||
<SelectItem key={project.id} value={String(project.id)}>
|
<SelectItem key={project.uuid || project.id} value={String(project.id || project.uuid)}>
|
||||||
{project.projectCode} - {project.projectName}
|
{project.projectCode} - {project.projectName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ export default function ContractVolumesPage() {
|
|||||||
)}
|
)}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{projects.map((project: { id: number; projectName: string; projectCode: string }) => (
|
{(projects as any[]).map((project) => (
|
||||||
<SelectItem key={project.id} value={String(project.id)}>
|
<SelectItem key={project.uuid || project.id} value={String(project.id || project.uuid)}>
|
||||||
{project.projectCode} - {project.projectName}
|
{project.projectCode} - {project.projectName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ export default function ShopMainCategoriesPage() {
|
|||||||
)}
|
)}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{projects.map((project: { id: number; projectName: string; projectCode: string }) => (
|
{(projects as any[]).map((project) => (
|
||||||
<SelectItem key={project.id} value={String(project.id)}>
|
<SelectItem key={project.uuid || project.id} value={String(project.id || project.uuid)}>
|
||||||
{project.projectCode} - {project.projectName}
|
{project.projectCode} - {project.projectName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ export default function ShopSubCategoriesPage() {
|
|||||||
)}
|
)}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{projects.map((project: { id: number; projectName: string; projectCode: string }) => (
|
{(projects as any[]).map((project) => (
|
||||||
<SelectItem key={project.id} value={String(project.id)}>
|
<SelectItem key={project.uuid || project.id} value={String(project.id || project.uuid)}>
|
||||||
{project.projectCode} - {project.projectName}
|
{project.projectCode} - {project.projectName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -24,29 +24,37 @@ import { BulkImportForm } from '@/components/numbering/bulk-import-form';
|
|||||||
|
|
||||||
export default function NumberingPage() {
|
export default function NumberingPage() {
|
||||||
const { data: projects = [] } = useProjects();
|
const { data: projects = [] } = useProjects();
|
||||||
const [selectedProjectId, setSelectedProjectId] = useState('1');
|
// Initialize with empty string or first project if available
|
||||||
|
const [selectedProjectId, setSelectedProjectId] = useState<string>('');
|
||||||
const [activeTab, setActiveTab] = useState('templates');
|
const [activeTab, setActiveTab] = useState('templates');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (projects.length > 0 && !selectedProjectId) {
|
||||||
|
const first = projects[0] as any;
|
||||||
|
setSelectedProjectId(String(first.id || first.uuid));
|
||||||
|
}
|
||||||
|
}, [projects, selectedProjectId]);
|
||||||
|
|
||||||
// View states
|
// View states
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [activeTemplate, setActiveTemplate] = useState<NumberingTemplate | undefined>(undefined);
|
const [activeTemplate, setActiveTemplate] = useState<NumberingTemplate | undefined>(undefined);
|
||||||
const [isTesting, setIsTesting] = useState(false);
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
const [testTemplate, setTestTemplate] = useState<NumberingTemplate | null>(null);
|
const [testTemplate, setTestTemplate] = useState<NumberingTemplate | null>(null);
|
||||||
|
|
||||||
const selectedProjectName =
|
const selectedProject = projects.find((p: any) => String(p.id || p.uuid) === selectedProjectId) as any;
|
||||||
projects.find((p: { id: number; projectName: string }) => p.id.toString() === selectedProjectId)?.projectName ||
|
const selectedProjectName = selectedProject?.projectName || 'Unknown Project';
|
||||||
'Unknown Project';
|
|
||||||
|
|
||||||
// Master Data
|
// Master Data
|
||||||
const { data: correspondenceTypes = [] } = useCorrespondenceTypes();
|
const { data: correspondenceTypes = [] } = useCorrespondenceTypes();
|
||||||
const { data: contracts = [] } = useContracts(Number(selectedProjectId));
|
const { data: contracts = [] } = useContracts(selectedProjectId as any); // Passing UUID/ID string
|
||||||
const contractId = contracts[0]?.id;
|
const firstContract = contracts[0] as any;
|
||||||
|
const contractId = firstContract?.id || firstContract?.uuid;
|
||||||
const { data: disciplines = [] } = useDisciplines(contractId);
|
const { data: disciplines = [] } = useDisciplines(contractId);
|
||||||
|
|
||||||
const { data: templateResponse, isLoading: isLoadingTemplates } = useTemplates();
|
const { data: templateResponse, isLoading: isLoadingTemplates } = useTemplates();
|
||||||
const saveTemplateMutation = useSaveTemplate();
|
const saveTemplateMutation = useSaveTemplate();
|
||||||
|
|
||||||
// Extract templates array from response (handles both direct array and { data: array } formats)
|
// Extract templates array from response
|
||||||
const templates: NumberingTemplate[] = Array.isArray(templateResponse)
|
const templates: NumberingTemplate[] = Array.isArray(templateResponse)
|
||||||
? templateResponse
|
? templateResponse
|
||||||
: ((templateResponse as any)?.data ?? []);
|
: ((templateResponse as any)?.data ?? []);
|
||||||
@@ -76,7 +84,7 @@ export default function NumberingPage() {
|
|||||||
<div className="p-6 max-w-4xl mx-auto animate-in fade-in slide-in-from-bottom-4">
|
<div className="p-6 max-w-4xl mx-auto animate-in fade-in slide-in-from-bottom-4">
|
||||||
<TemplateEditor
|
<TemplateEditor
|
||||||
template={activeTemplate}
|
template={activeTemplate}
|
||||||
projectId={Number(selectedProjectId)}
|
projectId={selectedProjectId as any}
|
||||||
projectName={selectedProjectName}
|
projectName={selectedProjectName}
|
||||||
correspondenceTypes={correspondenceTypes}
|
correspondenceTypes={correspondenceTypes}
|
||||||
disciplines={disciplines}
|
disciplines={disciplines}
|
||||||
@@ -100,8 +108,8 @@ export default function NumberingPage() {
|
|||||||
<SelectValue placeholder="Select Project" />
|
<SelectValue placeholder="Select Project" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{projects.map((project: { id: number; projectCode: string; projectName: string }) => (
|
{(projects as any[]).map((project) => (
|
||||||
<SelectItem key={project.id} value={project.id.toString()}>
|
<SelectItem key={project.uuid || project.id} value={String(project.id || project.uuid)}>
|
||||||
{project.projectCode} - {project.projectName}
|
{project.projectCode} - {project.projectName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
@@ -129,7 +137,7 @@ export default function NumberingPage() {
|
|||||||
<div className="lg:col-span-2 space-y-4">
|
<div className="lg:col-span-2 space-y-4">
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
{templates
|
{templates
|
||||||
.filter((t) => !t.projectId || t.projectId === Number(selectedProjectId))
|
.filter((t: any) => !t.projectId || String(t.projectId) === selectedProjectId || t.project?.uuid === selectedProjectId)
|
||||||
.map((template) => (
|
.map((template) => (
|
||||||
<Card key={template.id} className="p-6 hover:shadow-md transition-shadow">
|
<Card key={template.id} className="p-6 hover:shadow-md transition-shadow">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
@@ -194,11 +202,11 @@ export default function NumberingPage() {
|
|||||||
|
|
||||||
<TabsContent value="tools" className="space-y-4">
|
<TabsContent value="tools" className="space-y-4">
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<ManualOverrideForm projectId={Number(selectedProjectId)} />
|
<ManualOverrideForm projectId={selectedProjectId as any} />
|
||||||
<VoidReplaceForm projectId={Number(selectedProjectId)} />
|
<VoidReplaceForm projectId={selectedProjectId as any} />
|
||||||
<CancelNumberForm />
|
<CancelNumberForm />
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<BulkImportForm projectId={Number(selectedProjectId)} />
|
<BulkImportForm projectId={selectedProjectId as any} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ export default function DisciplinesPage() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const contractOptions = contracts.map((c) => ({
|
const contractOptions = contracts.map((c: any) => ({
|
||||||
label: `${c.contractName} (${c.contractCode})`,
|
label: `${c.contractName} (${c.contractCode})`,
|
||||||
value: c.id,
|
value: String(c.id || c.uuid),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -55,8 +55,8 @@ export default function DisciplinesPage() {
|
|||||||
title="Disciplines Management"
|
title="Disciplines Management"
|
||||||
description="Manage system disciplines (e.g., ARCH, STR, MEC)"
|
description="Manage system disciplines (e.g., ARCH, STR, MEC)"
|
||||||
queryKey={['disciplines', selectedContractId ?? 'all']}
|
queryKey={['disciplines', selectedContractId ?? 'all']}
|
||||||
fetchFn={() => masterDataService.getDisciplines(selectedContractId ? parseInt(selectedContractId) : undefined)}
|
fetchFn={() => masterDataService.getDisciplines(selectedContractId ? selectedContractId : undefined)}
|
||||||
createFn={(data: Record<string, unknown>) => masterDataService.createDiscipline(data as unknown as Parameters<typeof masterDataService.createDiscipline>[0])}
|
createFn={(data: Record<string, unknown>) => masterDataService.createDiscipline(data as any)}
|
||||||
updateFn={(id, data) => Promise.reject('Not implemented yet')}
|
updateFn={(id, data) => Promise.reject('Not implemented yet')}
|
||||||
deleteFn={(id) => masterDataService.deleteDiscipline(id)}
|
deleteFn={(id) => masterDataService.deleteDiscipline(id)}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -71,8 +71,8 @@ export default function DisciplinesPage() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">All Contracts</SelectItem>
|
<SelectItem value="all">All Contracts</SelectItem>
|
||||||
{contracts.map((c) => (
|
{contracts.map((c: any) => (
|
||||||
<SelectItem key={c.id} value={c.id.toString()}>
|
<SelectItem key={c.uuid || c.id} value={String(c.id || c.uuid)}>
|
||||||
{c.contractName} ({c.contractCode})
|
{c.contractName} ({c.contractCode})
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ export default function RfaTypesPage() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const contractOptions = contracts.map((c) => ({
|
const contractOptions = contracts.map((c: any) => ({
|
||||||
label: `${c.contractName} (${c.contractCode})`,
|
label: `${c.contractName} (${c.contractCode})`,
|
||||||
value: c.id,
|
value: String(c.id || c.uuid),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -58,8 +58,8 @@ export default function RfaTypesPage() {
|
|||||||
entityName="RFA Type"
|
entityName="RFA Type"
|
||||||
title="RFA Types Management"
|
title="RFA Types Management"
|
||||||
queryKey={['rfa-types', selectedContractId ?? 'all']}
|
queryKey={['rfa-types', selectedContractId ?? 'all']}
|
||||||
fetchFn={() => masterDataService.getRfaTypes(selectedContractId ? parseInt(selectedContractId) : undefined)}
|
fetchFn={() => masterDataService.getRfaTypes(selectedContractId ? selectedContractId : undefined)}
|
||||||
createFn={(data: Record<string, unknown>) => masterDataService.createRfaType(data as unknown as any)}
|
createFn={(data: Record<string, unknown>) => masterDataService.createRfaType(data as any)}
|
||||||
updateFn={(id, data) => masterDataService.updateRfaType(id, data)}
|
updateFn={(id, data) => masterDataService.updateRfaType(id, data)}
|
||||||
deleteFn={(id) => masterDataService.deleteRfaType(id)}
|
deleteFn={(id) => masterDataService.deleteRfaType(id)}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -74,8 +74,8 @@ export default function RfaTypesPage() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">All Contracts</SelectItem>
|
<SelectItem value="all">All Contracts</SelectItem>
|
||||||
{contracts.map((c) => (
|
{contracts.map((c: any) => (
|
||||||
<SelectItem key={c.id} value={c.id.toString()}>
|
<SelectItem key={c.uuid || c.id} value={String(c.id || c.uuid)}>
|
||||||
{c.contractName} ({c.contractCode})
|
{c.contractName} ({c.contractCode})
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { DataTable } from "@/components/common/data-table";
|
import {
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
ColumnDef,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Plus, Pencil, Trash2, Loader2 } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -11,20 +24,11 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Plus, Pencil, Trash2, RefreshCw } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -35,31 +39,41 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
|
||||||
interface FieldConfig {
|
interface Field {
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
type: "text" | "textarea" | "checkbox" | "select";
|
type: "text" | "number" | "checkbox" | "select" | "textarea";
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
options?: { label: string; value: string | number | boolean }[];
|
options?: { label: string; value: string | number }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GenericCrudTableProps<TEntity extends { id: number }> {
|
interface GenericCrudTableProps<T> {
|
||||||
entityName: string;
|
title: string;
|
||||||
queryKey: string[];
|
|
||||||
fetchFn: () => Promise<TEntity[]>;
|
|
||||||
createFn: (data: Record<string, unknown>) => Promise<TEntity>;
|
|
||||||
updateFn: (id: number, data: Record<string, unknown>) => Promise<TEntity>;
|
|
||||||
deleteFn: (id: number) => Promise<unknown>;
|
|
||||||
columns: ColumnDef<TEntity>[];
|
|
||||||
fields: FieldConfig[];
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
description?: string;
|
||||||
|
entityName: string;
|
||||||
|
queryKey: any[];
|
||||||
|
fetchFn: () => Promise<T[] | { data: T[] }>;
|
||||||
|
createFn: (data: any) => Promise<any>;
|
||||||
|
updateFn: (id: number, data: any) => Promise<any>;
|
||||||
|
deleteFn: (id: number) => Promise<any>;
|
||||||
|
columns: ColumnDef<T>[];
|
||||||
|
fields: Field[];
|
||||||
filters?: React.ReactNode;
|
filters?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GenericCrudTable<TEntity extends { id: number }>({
|
export function GenericCrudTable<T extends { id?: number; uuid?: string }>({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
entityName,
|
entityName,
|
||||||
queryKey,
|
queryKey,
|
||||||
fetchFn,
|
fetchFn,
|
||||||
@@ -68,105 +82,77 @@ export function GenericCrudTable<TEntity extends { id: number }>({
|
|||||||
deleteFn,
|
deleteFn,
|
||||||
columns,
|
columns,
|
||||||
fields,
|
fields,
|
||||||
title,
|
|
||||||
description,
|
|
||||||
filters,
|
filters,
|
||||||
}: GenericCrudTableProps<TEntity>) {
|
}: GenericCrudTableProps<T>) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [editingItem, setEditingItem] = useState<TEntity | null>(null);
|
const [editingItem, setEditingId] = useState<number | null>(null);
|
||||||
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
|
||||||
|
|
||||||
// Delete Dialog State
|
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
||||||
const [itemToDelete, setItemToDelete] = useState<number | null>(null);
|
const [itemToDelete, setItemToDelete] = useState<number | null>(null);
|
||||||
|
|
||||||
const { data, isLoading, refetch } = useQuery({
|
const { data: rawData, isLoading, refetch } = useQuery({
|
||||||
queryKey,
|
queryKey,
|
||||||
queryFn: fetchFn,
|
queryFn: fetchFn,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ADR-019: Support both direct array or wrapped data object
|
||||||
|
const data: T[] = Array.isArray(rawData) ? rawData : (rawData as any)?.data || [];
|
||||||
|
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: createFn,
|
mutationFn: createFn,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(`${entityName} created successfully`);
|
|
||||||
queryClient.invalidateQueries({ queryKey });
|
queryClient.invalidateQueries({ queryKey });
|
||||||
handleClose();
|
toast.success(`${entityName} created successfully`);
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
reset();
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.response?.data?.message || `Failed to create ${entityName}`);
|
||||||
},
|
},
|
||||||
onError: () => toast.error(`Failed to create ${entityName}`),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: ({ id, data }: { id: number; data: Record<string, unknown> }) => updateFn(id, data),
|
mutationFn: ({ id, data }: { id: number; data: any }) => updateFn(id, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(`${entityName} updated successfully`);
|
|
||||||
queryClient.invalidateQueries({ queryKey });
|
queryClient.invalidateQueries({ queryKey });
|
||||||
handleClose();
|
toast.success(`${entityName} updated successfully`);
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
setEditingId(null);
|
||||||
|
reset();
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.response?.data?.message || `Failed to update ${entityName}`);
|
||||||
},
|
},
|
||||||
onError: () => toast.error(`Failed to update ${entityName}`),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: deleteFn,
|
mutationFn: deleteFn,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(`${entityName} deleted successfully`);
|
|
||||||
queryClient.invalidateQueries({ queryKey });
|
queryClient.invalidateQueries({ queryKey });
|
||||||
setDeleteDialogOpen(false);
|
toast.success(`${entityName} deleted successfully`);
|
||||||
setItemToDelete(null);
|
setItemToDelete(null);
|
||||||
},
|
},
|
||||||
onError: () => toast.error(`Failed to delete ${entityName}`),
|
onError: (error: any) => {
|
||||||
|
toast.error(error.response?.data?.message || `Failed to delete ${entityName}`);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCreate = () => {
|
const {
|
||||||
setEditingItem(null);
|
register,
|
||||||
setFormData({});
|
handleSubmit,
|
||||||
setIsOpen(true);
|
reset,
|
||||||
};
|
setValue,
|
||||||
|
watch,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm();
|
||||||
|
|
||||||
const handleEdit = (item: TEntity) => {
|
const table = useReactTable({
|
||||||
setEditingItem(item);
|
data,
|
||||||
setFormData({ ...item });
|
columns: [
|
||||||
setIsOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteClick = (id: number) => {
|
|
||||||
setItemToDelete(id);
|
|
||||||
setDeleteDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmDelete = () => {
|
|
||||||
if (itemToDelete) {
|
|
||||||
deleteMutation.mutate(itemToDelete);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setIsOpen(false);
|
|
||||||
setEditingItem(null);
|
|
||||||
setFormData({});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (editingItem) {
|
|
||||||
updateMutation.mutate({ id: editingItem.id, data: formData });
|
|
||||||
} else {
|
|
||||||
createMutation.mutate(formData);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (field: string, value: unknown) => {
|
|
||||||
setFormData((prev: Record<string, unknown>) => ({ ...prev, [field]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add default Actions column if not present
|
|
||||||
const tableColumns = [
|
|
||||||
...columns,
|
...columns,
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
header: "Actions",
|
cell: ({ row }) => (
|
||||||
cell: ({ row }: { row: { original: TEntity } }) => (
|
<div className="flex justify-end gap-2">
|
||||||
<div className="flex gap-2 justify-end">
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -177,144 +163,221 @@ export function GenericCrudTable<TEntity extends { id: number }>({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="text-destructive"
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
onClick={() => handleDeleteClick(row.original.id)}
|
onClick={() => setItemToDelete(row.original.id as number)}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
],
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
setEditingId(null);
|
||||||
|
reset();
|
||||||
|
fields.forEach((f) => {
|
||||||
|
if (f.type === "checkbox") setValue(f.name, true);
|
||||||
|
});
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (item: any) => {
|
||||||
|
setEditingId(item.id);
|
||||||
|
reset(item);
|
||||||
|
// Ensure select values are strings for Shadcn Select
|
||||||
|
fields.forEach(f => {
|
||||||
|
if (f.type === 'select' && item[f.name]) {
|
||||||
|
setValue(f.name, String(item[f.name]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = (formData: any) => {
|
||||||
|
if (editingItem) {
|
||||||
|
updateMutation.mutate({ id: editingItem, data: formData });
|
||||||
|
} else {
|
||||||
|
createMutation.mutate(formData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
{title && <h2 className="text-xl font-bold">{title}</h2>}
|
<h2 className="text-2xl font-bold tracking-tight">{title}</h2>
|
||||||
{description && (
|
{description && (
|
||||||
<p className="text-sm text-muted-foreground">{description}</p>
|
<p className="text-muted-foreground">{description}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<Button onClick={handleAdd}>
|
||||||
{filters}
|
<Plus className="h-4 w-4 mr-2" /> Add {entityName}
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => refetch()}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
<RefreshCw className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleCreate}>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
Add {entityName}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading ? (
|
{filters && <div className="py-2">{filters}</div>}
|
||||||
<div className="space-y-2">
|
|
||||||
{[1, 2, 3, 4, 5].map((i) => (
|
<div className="rounded-md border bg-card">
|
||||||
<div key={i} className="flex items-center space-x-4">
|
<Table>
|
||||||
<Skeleton className="h-12 w-full" />
|
<TableHeader>
|
||||||
</div>
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
))}
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{isLoading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length + 1}
|
||||||
|
className="h-24 text-center"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : data.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length + 1}
|
||||||
|
className="h-24 text-center text-muted-foreground"
|
||||||
|
>
|
||||||
|
No data found.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
<DataTable columns={tableColumns} data={data || []} />
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow key={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
)}
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent className="max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{editingItem ? `Edit ${entityName}` : `New ${entityName}`}
|
{editingItem ? `Edit ${entityName}` : `Add New ${entityName}`}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 py-4">
|
||||||
{fields.map((field) => (
|
{fields.map((field) => (
|
||||||
<div key={field.name} className="space-y-2">
|
<div key={field.name} className="space-y-2">
|
||||||
<Label htmlFor={field.name}>{field.label}</Label>
|
<Label htmlFor={field.name}>
|
||||||
{field.type === "textarea" ? (
|
{field.label} {field.required && "*"}
|
||||||
<Textarea
|
</Label>
|
||||||
id={field.name}
|
{field.type === "checkbox" ? (
|
||||||
value={(formData[field.name] as string) || ""}
|
|
||||||
onChange={(e) => handleChange(field.name, e.target.value)}
|
|
||||||
required={field.required}
|
|
||||||
/>
|
|
||||||
) : field.type === "checkbox" ? (
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id={field.name}
|
id={field.name}
|
||||||
checked={!!formData[field.name]}
|
checked={watch(field.name)}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) => setValue(field.name, checked)}
|
||||||
handleChange(field.name, checked)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<label htmlFor={field.name} className="text-sm">
|
<label
|
||||||
Enabled
|
htmlFor={field.name}
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
Active
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
) : field.type === "select" ? (
|
) : field.type === "select" ? (
|
||||||
<Select
|
<Select
|
||||||
value={formData[field.name]?.toString() || ""}
|
value={String(watch(field.name) || "")}
|
||||||
onValueChange={(value) => handleChange(field.name, value)}
|
onValueChange={(val) => setValue(field.name, val)}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={`Select ${field.label}`} />
|
<SelectValue placeholder={`Select ${field.label}...`} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{field.options?.map((opt) => (
|
{field.options?.map((opt) => (
|
||||||
<SelectItem key={(opt.value as string | number)} value={opt.value.toString()}>
|
<SelectItem key={opt.value} value={String(opt.value)}>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
) : field.type === "textarea" ? (
|
||||||
|
<Textarea
|
||||||
|
id={field.name}
|
||||||
|
{...register(field.name, { required: field.required })}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
id={field.name}
|
id={field.name}
|
||||||
type="text"
|
type={field.type}
|
||||||
value={(formData[field.name] as string) || ""}
|
{...register(field.name, {
|
||||||
onChange={(e) => handleChange(field.name, e.target.value)}
|
required: field.required,
|
||||||
required={field.required}
|
valueAsNumber: field.type === "number",
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{errors[field.name] && (
|
||||||
|
<p className="text-xs text-red-500 font-medium">
|
||||||
|
{field.label} is required
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleClose}
|
onClick={() => setIsDialogOpen(false)}
|
||||||
disabled={createMutation.isPending || updateMutation.isPending}
|
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="submit" disabled={createMutation.isPending || updateMutation.isPending}>
|
||||||
type="submit"
|
{(createMutation.isPending || updateMutation.isPending) && (
|
||||||
disabled={createMutation.isPending || updateMutation.isPending}
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
>
|
)}
|
||||||
{editingItem ? "Update" : "Create"}
|
{editingItem ? "Save Changes" : `Add ${entityName}`}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<AlertDialog
|
||||||
|
open={itemToDelete !== null}
|
||||||
|
onOpenChange={(open) => !open && setItemToDelete(null)}
|
||||||
|
>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This action cannot be undone. This will permanently delete the {entityName.toLowerCase()} and remove it from the system.
|
This action cannot be undone. This will permanently delete this{" "}
|
||||||
|
{entityName.toLowerCase()} and remove its data from our servers.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={confirmDelete}
|
onClick={() => itemToDelete && deleteMutation.mutate(itemToDelete)}
|
||||||
className="bg-red-600 hover:bg-red-700"
|
className="bg-red-600 hover:bg-red-700"
|
||||||
>
|
>
|
||||||
{deleteMutation.isPending ? "Deleting..." : "Delete"}
|
{deleteMutation.isPending ? "Deleting..." : "Delete"}
|
||||||
|
|||||||
Reference in New Issue
Block a user