chore: import DH V2 D5.6.4 production baseline

This commit is contained in:
DH V2
2026-09-05 10:12:35 -03:00
commit 82213e72f5
757 changed files with 84218 additions and 0 deletions
@@ -0,0 +1,16 @@
import { Transform } from 'class-transformer';
import { IsEnum, IsOptional, IsString, MaxLength } from 'class-validator';
import { InspectionVisitStatus } from '../../database/entities';
export class ChangeInspectionVisitStatusDto {
@IsEnum(InspectionVisitStatus)
status!: InspectionVisitStatus;
@IsOptional()
@Transform(({ value }) =>
typeof value === 'string' && value.trim() ? value.trim() : null,
)
@IsString()
@MaxLength(4000)
reason?: string | null;
}
@@ -0,0 +1,15 @@
import { IsISO8601, IsUUID } from 'class-validator';
export class CreateInspectionVisitDto {
@IsUUID('4')
operationalAreaId!: string;
@IsUUID('4')
operatorCompanyId!: string;
@IsISO8601({ strict: true })
plannedStartAt!: string;
@IsUUID('4')
leadInspectorUserId!: string;
}
@@ -0,0 +1,10 @@
import { Transform } from 'class-transformer';
import { IsString, MaxLength, MinLength } from 'class-validator';
export class ExcludeInspectionVisitAssetDto {
@Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
@IsString()
@MinLength(10)
@MaxLength(2000)
reason!: string;
}
@@ -0,0 +1,48 @@
import { Type } from 'class-transformer';
import { IsEnum, IsInt, IsOptional, IsString, IsUUID, Matches, Max, MaxLength, Min } from 'class-validator';
import { InspectionVisitStatus } from '../../database/entities';
export class ListInspectionVisitsQueryDto {
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page = 1;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
pageSize = 25;
@IsOptional()
@IsString()
@MaxLength(200)
search?: string;
@IsOptional()
@IsEnum(InspectionVisitStatus)
status?: InspectionVisitStatus;
@IsOptional()
@IsUUID('4')
companyId?: string;
@IsOptional()
@IsUUID('4')
areaId?: string;
@IsOptional()
@IsUUID('4')
inspectorId?: string;
@IsOptional()
@Matches(/^\d{4}-\d{2}-\d{2}$/)
dateFrom?: string;
@IsOptional()
@Matches(/^\d{4}-\d{2}-\d{2}$/)
dateTo?: string;
}
@@ -0,0 +1,9 @@
import { ArrayMaxSize, ArrayUnique, IsArray, IsUUID } from 'class-validator';
export class ReplaceInspectionVisitAssetsDto {
@IsArray()
@ArrayMaxSize(500)
@ArrayUnique()
@IsUUID('4', { each: true })
assetIds!: string[];
}
@@ -0,0 +1,13 @@
import { ArrayMaxSize, ArrayUnique, IsArray, IsOptional, IsUUID } from 'class-validator';
export class ReplaceInspectionVisitTeamDto {
@IsOptional()
@IsUUID('4')
leadInspectorUserId!: string | null;
@IsArray()
@ArrayMaxSize(50)
@ArrayUnique()
@IsUUID('4', { each: true })
memberUserIds!: string[];
}
@@ -0,0 +1,42 @@
import { Transform } from 'class-transformer';
import {
IsISO8601,
IsOptional,
IsString,
IsUUID,
MaxLength,
} from 'class-validator';
export class UpdateInspectionVisitDto {
@IsOptional()
@Transform(({ value }) =>
typeof value === 'string' && value.trim() ? value.trim() : null,
)
@IsString()
@MaxLength(4000)
objective?: string | null;
@IsOptional()
@IsUUID('4')
scopeAssetId?: string | null;
@IsOptional()
@IsUUID('4')
operationalAreaId?: string | null;
@IsOptional()
@IsUUID('4')
operatorCompanyId?: string | null;
@IsOptional()
@IsISO8601({ strict: true })
plannedStartAt?: string | null;
@IsOptional()
@Transform(({ value }) =>
typeof value === 'string' && value.trim() ? value.trim() : null,
)
@IsString()
@MaxLength(4000)
instructions?: string | null;
}
@@ -0,0 +1,34 @@
import type { EntityManager } from 'typeorm';
export async function nextInspectionVisitCode(
manager: EntityManager,
plannedStartAt: Date,
): Promise<string> {
const [yearRow] = (await manager.query(`
SELECT EXTRACT(
YEAR FROM $1::timestamptz AT TIME ZONE 'America/Argentina/Mendoza'
)::integer AS year
`, [plannedStartAt])) as Array<{ year: number }>;
const year = Number(yearRow?.year);
if (!Number.isInteger(year) || year < 2000 || year > 9999) {
throw new Error('Unable to derive inspection visit year');
}
await manager.query(
`SELECT pg_advisory_xact_lock(hashtext($1))`,
[`inspection-visit-code:${year}`],
);
const [sequenceRow] = (await manager.query(`
SELECT COALESCE(MAX(RIGHT(code, 6)::integer), 0)::integer AS sequence
FROM inspection_visits
WHERE code LIKE $1
AND code ~ ('^INS-' || $2::text || '-[0-9]{6}$')
`, [`INS-${year}-%`, year])) as Array<{ sequence: number }>;
const sequence = Number(sequenceRow?.sequence ?? 0) + 1;
if (sequence > 999999) {
throw new Error(`Inspection visit sequence exhausted for ${year}`);
}
return `INS-${year}-${String(sequence).padStart(6, '0')}`;
}
@@ -0,0 +1,157 @@
import {
Body,
Controller,
Get,
Param,
ParseUUIDPipe,
Patch,
Post,
Put,
Query,
Req,
} from '@nestjs/common';
import { CurrentAuth } from '../auth/decorators/current-auth.decorator';
import { RequirePermissions } from '../authorization/decorators/require-permissions.decorator';
import type { AuthPrincipal, RequestWithContext } from '../common/http/request-context';
import { ChangeInspectionVisitStatusDto } from './dto/change-inspection-visit-status.dto';
import { CreateInspectionVisitDto } from './dto/create-inspection-visit.dto';
import { ExcludeInspectionVisitAssetDto } from './dto/exclude-inspection-visit-asset.dto';
import { ListInspectionVisitsQueryDto } from './dto/list-inspection-visits-query.dto';
import { ReplaceInspectionVisitAssetsDto } from './dto/replace-inspection-visit-assets.dto';
import { ReplaceInspectionVisitTeamDto } from './dto/replace-inspection-visit-team.dto';
import { UpdateInspectionVisitDto } from './dto/update-inspection-visit.dto';
import { InspectionVisitsService } from './inspection-visits.service';
@Controller('inspection-visits')
export class InspectionVisitsController {
constructor(private readonly visits: InspectionVisitsService) {}
@Get()
@RequirePermissions('inspections.read')
list(@Query() query: ListInspectionVisitsQueryDto) {
return this.visits.list(query);
}
@Get('assignees')
@RequirePermissions('inspections.assign')
assignees() {
return this.visits.listAssignees();
}
@Get('planning-context/areas')
@RequirePermissions('inspections.read')
planningAreas() {
return this.visits.listPlanningAreas();
}
@Get('planning-context/areas/:areaId/operators')
@RequirePermissions('inspections.read')
planningOperators(
@Param('areaId', new ParseUUIDPipe({ version: '4' })) areaId: string,
) {
return this.visits.listPlanningOperators(areaId);
}
@Get(':id')
@RequirePermissions('inspections.read')
get(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
return this.visits.getById(id);
}
@Post()
@RequirePermissions('inspections.manage', 'inspections.assign')
create(
@Body() dto: CreateInspectionVisitDto,
@CurrentAuth() principal: AuthPrincipal,
@Req() request: RequestWithContext,
) {
return this.visits.create(dto, principal, request);
}
@Patch(':id')
@RequirePermissions('inspections.manage')
update(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
@Body() dto: UpdateInspectionVisitDto,
@CurrentAuth() principal: AuthPrincipal,
@Req() request: RequestWithContext,
) {
return this.visits.update(id, dto, principal, request);
}
@Put(':id/assets')
@RequirePermissions('inspections.manage')
replaceAssets(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
@Body() dto: ReplaceInspectionVisitAssetsDto,
@CurrentAuth() principal: AuthPrincipal,
@Req() request: RequestWithContext,
) {
return this.visits.replaceAssets(id, dto, principal, request);
}
@Post(':id/checklist/generate')
@RequirePermissions('inspections.manage')
generateChecklist(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
@CurrentAuth() principal: AuthPrincipal,
@Req() request: RequestWithContext,
) {
return this.visits.generateChecklist(id, principal, request);
}
@Post(':id/assets/:assetId/exclude')
@RequirePermissions('inspections.manage')
excludeAsset(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
@Param('assetId', new ParseUUIDPipe({ version: '4' })) assetId: string,
@Body() dto: ExcludeInspectionVisitAssetDto,
@CurrentAuth() principal: AuthPrincipal,
@Req() request: RequestWithContext,
) {
return this.visits.excludeAsset(id, assetId, dto.reason, principal, request);
}
@Post(':id/assets/:assetId/include')
@RequirePermissions('inspections.manage')
includeAsset(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
@Param('assetId', new ParseUUIDPipe({ version: '4' })) assetId: string,
@CurrentAuth() principal: AuthPrincipal,
@Req() request: RequestWithContext,
) {
return this.visits.includeAsset(id, assetId, principal, request);
}
@Put(':id/team')
@RequirePermissions('inspections.assign')
replaceTeam(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
@Body() dto: ReplaceInspectionVisitTeamDto,
@CurrentAuth() principal: AuthPrincipal,
@Req() request: RequestWithContext,
) {
return this.visits.replaceTeam(id, dto, principal, request);
}
@Put(':id/status')
@RequirePermissions('inspections.manage')
changeStatus(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
@Body() dto: ChangeInspectionVisitStatusDto,
@CurrentAuth() principal: AuthPrincipal,
@Req() request: RequestWithContext,
) {
return this.visits.changeStatus(id, dto, principal, request);
}
@Post(':id/start')
@RequirePermissions('inspections.execute')
start(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
@CurrentAuth() principal: AuthPrincipal,
@Req() request: RequestWithContext,
) {
return this.visits.start(id, principal, request);
}
}
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { AuditModule } from '../audit/audit.module';
import { InspectionVisitsController } from './inspection-visits.controller';
import { InspectionVisitsService } from './inspection-visits.service';
@Module({
imports: [AuditModule],
controllers: [InspectionVisitsController],
providers: [InspectionVisitsService],
})
export class InspectionVisitsModule {}
File diff suppressed because it is too large Load Diff