Add field management module with database schema and validation

- Introduced `FieldModule` to manage cycles and plans, including read and write controllers.
- Created database migrations for `company_settings`, `cycles`, `cycle_weekdays`, `cycle_destinations`, `plans`, `plan_destinations`, `plan_invoices`, and `plan_packing_slips` tables, including constraints and unique indexes.
- Developed service and repository layers for handling cycle and plan data operations.
- Added unit tests for the cycles and plans services, repositories, and controllers to ensure functionality and correctness.
- Updated application module to include the new `FieldModule` for better organization.
This commit is contained in:
shancheas
2026-08-25 18:20:19 +07:00
parent 34d4f2c120
commit c9f9b31abf
53 changed files with 5990 additions and 5 deletions
@@ -0,0 +1,174 @@
import {
Body,
Controller,
Delete,
HttpCode,
Param,
ParseUUIDPipe,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import {
ApiBearerAuth,
ApiCreatedResponse,
ApiForbiddenResponse,
ApiNoContentResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import type { AuthUser } from '../../../common/auth/auth-user';
import { CurrentUser } from '../../../common/decorators/current-user.decorator';
import { BEARER_AUTH_NAME } from '../../../common/swagger/setup-swagger';
import { RequireFieldPrivilege } from '../shared/field-privilege.decorator';
import { FieldPrivilegeGuard } from '../shared/field-privilege.guard';
import {
AddPlanDestinationDto,
BulkIdsDto,
BulkStatusDto,
CreatePlanDto,
GeneratePlansDto,
PlanDto,
UpdatePlanDto,
UpdatePlanStatusDto,
} from './dto/plan.dto';
import { PlansService } from './plans.service';
@ApiTags('plans')
@ApiBearerAuth(BEARER_AUTH_NAME)
@UseGuards(FieldPrivilegeGuard)
@Controller('plans')
export class PlansWriteController {
constructor(private readonly plansService: PlansService) {}
@Post('generate')
@RequireFieldPrivilege('plan', 'create')
@ApiOperation({ summary: 'Generate plans from cycles for a date range' })
@ApiOkResponse({
schema: {
properties: {
created: { type: 'number' },
skipped: { type: 'number' },
},
},
})
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
generate(
@Body() dto: GeneratePlansDto,
@CurrentUser('id') userId: string,
): Promise<{ created: number; skipped: number }> {
return this.plansService.generate({ ...dto, userId });
}
@Post('bulk-delete')
@HttpCode(200)
@RequireFieldPrivilege('plan', 'delete')
@ApiOperation({ summary: 'Bulk archive plans' })
@ApiOkResponse({ schema: { properties: { deleted: { type: 'number' } } } })
bulkDelete(
@Body() dto: BulkIdsDto,
@CurrentUser() user: AuthUser,
): Promise<{ deleted: number }> {
return this.plansService.bulkDelete(dto.ids, user);
}
@Post('bulk-status')
@HttpCode(200)
@RequireFieldPrivilege('plan', 'update')
@ApiOperation({ summary: 'Bulk update plan status' })
@ApiOkResponse({ schema: { properties: { updated: { type: 'number' } } } })
bulkStatus(
@Body() dto: BulkStatusDto,
@CurrentUser() user: AuthUser,
): Promise<{ updated: number }> {
return this.plansService.bulkUpdateStatus(
dto.ids,
dto.status,
user.id,
user,
);
}
@Post()
@RequireFieldPrivilege('plan', 'create')
@ApiOperation({ summary: 'Create plan' })
@ApiCreatedResponse({ type: PlanDto })
create(
@Body() dto: CreatePlanDto,
@CurrentUser('id') userId: string,
): Promise<PlanDto> {
return this.plansService.create({ ...dto, userId });
}
@Post(':id/destinations')
@RequireFieldPrivilege('plan', 'update')
@ApiOperation({ summary: 'Add a destination to a plan' })
@ApiOkResponse({ type: PlanDto })
addDestination(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: AddPlanDestinationDto,
@CurrentUser() user: AuthUser,
): Promise<PlanDto> {
return this.plansService.addDestination(
id,
dto.customerId,
dto.afterDestinationId,
user,
);
}
@Delete(':id/destinations/:destinationId')
@RequireFieldPrivilege('plan', 'update')
@ApiOperation({ summary: 'Remove a destination from a plan' })
@ApiOkResponse({ type: PlanDto })
removeDestination(
@Param('id', ParseUUIDPipe) id: string,
@Param('destinationId', ParseUUIDPipe) destinationId: string,
@CurrentUser() user: AuthUser,
): Promise<PlanDto> {
return this.plansService.removeDestination(id, destinationId, user);
}
@Patch(':id/status')
@RequireFieldPrivilege('plan', 'update')
@ApiOperation({ summary: 'Update plan status' })
@ApiOkResponse({ type: PlanDto })
updateStatus(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdatePlanStatusDto,
@CurrentUser() user: AuthUser,
): Promise<PlanDto> {
return this.plansService.updateStatus(id, dto.status, user.id, user);
}
@Patch(':id')
@RequireFieldPrivilege('plan', 'update')
@ApiOperation({ summary: 'Update plan (not status)' })
@ApiOkResponse({ type: PlanDto })
@ApiNotFoundResponse()
update(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdatePlanDto,
@CurrentUser() user: AuthUser,
): Promise<PlanDto> {
return this.plansService.update(id, { ...dto, userId: user.id }, user);
}
@Delete(':id')
@HttpCode(204)
@RequireFieldPrivilege('plan', 'delete')
@ApiOperation({ summary: 'Archive plan' })
@ApiNoContentResponse()
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
async delete(
@Param('id', ParseUUIDPipe) id: string,
@CurrentUser() user: AuthUser,
): Promise<void> {
await this.plansService.delete(id, user);
}
}