diff --git a/src/app.module.ts b/src/app.module.ts index 93b7423..5e6ab5d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -31,6 +31,7 @@ import { VehicleUsageService } from './modules/vehicle-usage/vehicle-usage.servi import { MaterialsInventoryModule } from './modules/materials_inventory/materials_inventory.module'; import { CompanyModule } from './modules/company/company.module'; import { ProductModule } from './modules/product/product.module'; +import { ProjectModule } from './modules/project/project.module'; @Module({ imports: [ @@ -58,16 +59,23 @@ import { ProductModule } from './modules/product/product.module'; // end biz TodoModule, - + // 合同模块 ContractModule, + // 车辆管理 VehicleUsageModule, + // 原材料库存 MaterialsInventoryModule, + // 公司管理 CompanyModule, - ProductModule + // 产品管理 + ProductModule, + + // 项目管理 + ProjectModule ], providers: [ { provide: APP_FILTER, useClass: AllExceptionsFilter }, diff --git a/src/modules/materials_inventory/in_out/materials_in_out.dto.ts b/src/modules/materials_inventory/in_out/materials_in_out.dto.ts index 8ca15c1..ec41f22 100644 --- a/src/modules/materials_inventory/in_out/materials_in_out.dto.ts +++ b/src/modules/materials_inventory/in_out/materials_in_out.dto.ts @@ -22,7 +22,12 @@ import { Storage } from '~/modules/tools/storage/storage.entity'; import { formatToDate } from '~/utils'; export class MaterialsInOutDto { - @ApiProperty({ description: '产品' }) + @IsOptional() + @IsNumber() + @ApiProperty({ description: '项目Id' }) + projectId?: number; + + @ApiProperty({ description: '产品Id' }) @ValidateIf(o => !o.inventoryNumber) @IsNumber() productId: number; @@ -71,11 +76,6 @@ export class MaterialsInOutDto { @IsString() @ApiProperty({ description: '备注' }) remark: string; - - @IsOptional() - @IsString() - @ApiProperty({ description: '项目' }) - project: string; } export class MaterialsInOutUpdateDto extends PartialType(MaterialsInOutDto) { @@ -122,7 +122,6 @@ export class MaterialsInOutQueryDto extends PagerDto { @IsOptional() @IsString() inventoryNumber?: string; - @IsOptional() @IsString() diff --git a/src/modules/materials_inventory/in_out/materials_in_out.entity.ts b/src/modules/materials_inventory/in_out/materials_in_out.entity.ts index 5197eb1..833684f 100644 --- a/src/modules/materials_inventory/in_out/materials_in_out.entity.ts +++ b/src/modules/materials_inventory/in_out/materials_in_out.entity.ts @@ -16,6 +16,7 @@ import { import { CommonEntity } from '~/common/entity/common.entity'; import { MaterialsInOrOutEnum, ParamConfigEnum } from '~/constants/enum'; import { ProductEntity } from '~/modules/product/product.entity'; +import { ProjectEntity } from '~/modules/project/project.entity'; import { ParamConfigEntity } from '~/modules/system/param-config/param-config.entity'; import { Storage } from '~/modules/tools/storage/storage.entity'; @Entity({ name: 'materials_in_out' }) @@ -103,14 +104,18 @@ export class MaterialsInOutEntity extends CommonEntity { @ApiProperty({ description: '备注' }) remark: string; - @Column({ name: 'project', type: 'varchar', length: 255, comment: '项目', nullable: false }) - @ApiProperty({ description: '项目' }) - project: string; + @Column({ name: 'project_id', type: 'int', comment: '项目', nullable: true }) + @ApiProperty({ description: '项目Id' }) + projectId: number; @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) @ApiProperty({ description: '删除状态:0未删除,1已删除' }) isDelete: number; + @ManyToOne(() => ProjectEntity) + @JoinColumn({ name: 'project_id' }) + project: ProjectEntity; + @ManyToOne(() => ProductEntity) @JoinColumn({ name: 'product_id' }) product: ProductEntity; diff --git a/src/modules/materials_inventory/in_out/materials_in_out.service.ts b/src/modules/materials_inventory/in_out/materials_in_out.service.ts index 3c8b0c1..7c685aa 100644 --- a/src/modules/materials_inventory/in_out/materials_in_out.service.ts +++ b/src/modules/materials_inventory/in_out/materials_in_out.service.ts @@ -35,22 +35,28 @@ export class MaterialsInOutService { page, pageSize, product: productName, + project: projectName, isCreateOut, ...ext }: MaterialsInOutQueryDto): Promise> { const sqb = this.materialsInOutRepository .createQueryBuilder('materialsInOut') .leftJoin('materialsInOut.files', 'files') + .leftJoin('materialsInOut.project', 'project') .leftJoin('materialsInOut.product', 'product') .leftJoin('product.unit', 'unit') .leftJoin('product.company', 'company') - .addSelect(['files.path', 'product.name', 'unit.label', 'company.name']) + .addSelect(['files.path', 'project.name', 'product.name', 'unit.label', 'company.name']) .where(fieldSearch(ext)) .andWhere('materialsInOut.isDelete = 0') .addOrderBy('materialsInOut.createdAt', 'DESC'); if (productName) { sqb.andWhere('product.name like :productName', { productName: `%${productName}%` }); } + if (projectName) { + sqb.andWhere('project.name like :projectName', { projectName: `%${projectName}%` }); + } + if (isCreateOut) { sqb.andWhere('materialsInOut.inOrOut = 0'); } diff --git a/src/modules/project/project.controller.ts b/src/modules/project/project.controller.ts new file mode 100644 index 0000000..605090b --- /dev/null +++ b/src/modules/project/project.controller.ts @@ -0,0 +1,79 @@ +import { + Body, + Controller, + Get, + Query, + Put, + Delete, + Post, + BadRequestException +} from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Perm, definePermission } from '../auth/decorators/permission.decorator'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { ProjectService } from './project.service'; +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { ProjectEntity } from './project.entity'; +import { ProjectDto, ProjectQueryDto, ProjectUpdateDto } from './project.dto'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +export const permissions = definePermission('app:project', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('Project - 项目') +@ApiSecurityAuth() +@Controller('project') +export class ProjectController { + constructor(private projectService: ProjectService) {} + + @Get() + @ApiOperation({ summary: '分页获取项目列表' }) + @ApiResult({ type: [ProjectEntity], isPage: true }) + @Perm(permissions.LIST) + async list(@Query() dto: ProjectQueryDto) { + return this.projectService.findAll(dto); + } + + @Get(':id') + @ApiOperation({ summary: '获取项目信息' }) + @ApiResult({ type: ProjectDto }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.projectService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增项目' }) + @Perm(permissions.CREATE) + async create(@Body() dto: ProjectDto): Promise { + await this.projectService.create(dto); + } + + @Put(':id') + @ApiOperation({ summary: '更新项目' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: ProjectUpdateDto): Promise { + await this.projectService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除项目' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.projectService.delete(id); + } + + @Put('unlink-attachments/:id') + @ApiOperation({ summary: '附件解除关联' }) + @Perm(permissions.UPDATE) + async unlinkAttachments( + @IdParam() id: number, + @Body() { fileIds }: ProjectUpdateDto + ): Promise { + await this.projectService.unlinkAttachments(id, fileIds); + } +} diff --git a/src/modules/project/project.dto.ts b/src/modules/project/project.dto.ts new file mode 100644 index 0000000..6c96b6e --- /dev/null +++ b/src/modules/project/project.dto.ts @@ -0,0 +1,40 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { IsArray, IsOptional, IsString } from 'class-validator'; +import { PagerDto } from '~/common/dto/pager.dto'; +import { Storage } from '../tools/storage/storage.entity'; +import { IsUnique } from '~/shared/database/constraints/unique.constraint'; +import { ProjectEntity } from './project.entity'; + +export class ProjectDto { + @ApiProperty({ description: '项目名称' }) + @IsUnique(ProjectEntity, { message: '已存在同名项目' }) + @IsString() + name: string; + + @ApiProperty({ description: '附件' }) + files: Storage[]; +} + +export class ProjectUpdateDto extends PartialType(ProjectDto) { + @ApiProperty({ description: '附件' }) + @IsOptional() + @IsArray() + fileIds: number[]; +} + +export class ComapnyCreateDto extends PartialType(ProjectDto) { + @ApiProperty({ description: '附件' }) + @IsOptional() + @IsArray() + fileIds: number[]; +} + +export class ProjectQueryDto extends IntersectionType( + PagerDto, + PartialType(ProjectDto) +) { + @ApiProperty({ description: '项目名称' }) + @IsOptional() + @IsString() + name: string; +} diff --git a/src/modules/project/project.entity.ts b/src/modules/project/project.entity.ts new file mode 100644 index 0000000..a80b296 --- /dev/null +++ b/src/modules/project/project.entity.ts @@ -0,0 +1,38 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, JoinTable, ManyToMany, OneToMany, Relation } from 'typeorm'; +import { CommonEntity } from '~/common/entity/common.entity'; +import { Storage } from '../tools/storage/storage.entity'; +import { ProductEntity } from '../product/product.entity'; +import { MaterialsInOutEntity } from '../materials_inventory/in_out/materials_in_out.entity'; + +/** + * 项目实体类 + */ +@Entity({ name: 'project' }) +export class ProjectEntity extends CommonEntity { + @Column({ + name: 'name', + type: 'varchar', + unique: true, + length: 255, + comment: '项目名称' + }) + @ApiProperty({ description: '项目名称' }) + name: string; + + @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) + @ApiProperty({ description: '删除状态:0未删除,1已删除' }) + isDelete: number; + + @ApiHideProperty() + @OneToMany(() => MaterialsInOutEntity, product => product.project) + materialsInOuts: Relation; + + @ManyToMany(() => Storage, storage => storage.projects) + @JoinTable({ + name: 'project_storage', + joinColumn: { name: 'project_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'file_id', referencedColumnName: 'id' } + }) + files: Relation; +} diff --git a/src/modules/project/project.module.ts b/src/modules/project/project.module.ts new file mode 100644 index 0000000..170c6eb --- /dev/null +++ b/src/modules/project/project.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { ProjectController } from './project.controller'; +import { ProjectService } from './project.service'; +import { ProjectEntity } from './project.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { StorageModule } from '../tools/storage/storage.module'; +import { DatabaseModule } from '~/shared/database/database.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([ProjectEntity]), StorageModule, DatabaseModule], + controllers: [ProjectController], + providers: [ProjectService] +}) +export class ProjectModule {} diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts new file mode 100644 index 0000000..627299c --- /dev/null +++ b/src/modules/project/project.service.ts @@ -0,0 +1,127 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { ProjectEntity } from './project.entity'; +import { EntityManager, Like, Repository } from 'typeorm'; +import { ProjectDto, ProjectQueryDto, ProjectUpdateDto } from './project.dto'; +import { Pagination } from '~/helper/paginate/pagination'; +import { paginate } from '~/helper/paginate'; +import { Storage } from '../tools/storage/storage.entity'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { fieldSearch } from '~/shared/database/field-search'; + +@Injectable() +export class ProjectService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(ProjectEntity) + private projectRepository: Repository, + @InjectRepository(Storage) + private storageRepository: Repository + ) {} + + /** + * 分页查询所有 + */ + async findAll({ + page, + pageSize, + ...fields + }: ProjectQueryDto): Promise> { + const queryBuilder = this.projectRepository + .createQueryBuilder('project') + .leftJoin('project.files', 'files') + .addSelect(['files.id', 'files.path']) + .where(fieldSearch(fields)) + .andWhere('project.isDelete = 0'); + + return paginate(queryBuilder, { + page, + pageSize + }); + } + + /** + * 新增 + */ + async create(dto: ProjectDto): Promise { + await this.projectRepository.insert(dto); + } + + /** + * 更新 + */ + async update(id: number, { fileIds, ...data }: Partial): Promise { + await this.entityManager.transaction(async manager => { + await manager.update(ProjectEntity, id, { + ...data + }); + const project = await this.projectRepository + .createQueryBuilder('project') + .leftJoinAndSelect('project.files', 'files') + .where('project.id = :id', { id }) + .getOne(); + if (fileIds?.length) { + const count = await this.storageRepository + .createQueryBuilder('storage') + .where('storage.id in(:fileIds)', { fileIds }) + .getCount(); + if (count !== fileIds?.length) { + throw new BusinessException(ErrorEnum.STORAGE_NOT_FOUND); + } + // 附件要批量更新 + await manager + .createQueryBuilder() + .relation(ProjectEntity, 'files') + .of(id) + .addAndRemove(fileIds, project.files); + } + }); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + // 比较重要,做逻辑删除 + await this.projectRepository.update(id, { isDelete: 1 }); + } + + /** + * 获取单个信息 + */ + async info(id: number) { + const info = await this.projectRepository + .createQueryBuilder('project') + .where({ + id + }) + .andWhere('project.isDelete = 0') + .getOne(); + return info; + } + + /** + * 解除附件关联 + * @param id 实体ID + * @param fileIds 附件ID + */ + async unlinkAttachments(id: number, fileIds: number[]) { + await this.entityManager.transaction(async manager => { + const project = await this.projectRepository + .createQueryBuilder('project') + .leftJoinAndSelect('project.files', 'files') + .where('project.id = :id', { id }) + .getOne(); + const linkedFiles = project.files + .map(item => item.id) + .filter(item => !fileIds.includes(item)); + // 附件要批量更新 + await manager + .createQueryBuilder() + .relation(ProjectEntity, 'files') + .of(id) + .addAndRemove(linkedFiles, project.files); + }); + } +} diff --git a/src/modules/tools/storage/storage.entity.ts b/src/modules/tools/storage/storage.entity.ts index c1a59b7..ae0034a 100644 --- a/src/modules/tools/storage/storage.entity.ts +++ b/src/modules/tools/storage/storage.entity.ts @@ -7,6 +7,7 @@ import { ContractEntity } from '~/modules/contract/contract.entity'; import { MaterialsInOutEntity } from '~/modules/materials_inventory/in_out/materials_in_out.entity'; import { MaterialsInventoryEntity } from '~/modules/materials_inventory/materials_inventory.entity'; import { ProductEntity } from '~/modules/product/product.entity'; +import { ProjectEntity } from '~/modules/project/project.entity'; @Entity({ name: 'tool_storage' }) export class Storage extends CommonEntity { @@ -58,4 +59,8 @@ export class Storage extends CommonEntity { @ApiHideProperty() @ManyToMany(() => ProductEntity, product => product.files) products: Relation; + + @ApiHideProperty() + @ManyToMany(() => ProjectEntity, project => project.files) + projects: Relation; }