feat(warehouse): Batch 3+4 — export receive-selected label + bulk-mark-inspected workflow

- BulkReceiveModal: Receive Selected shows "received at facility" for EXPORT
- Export inspection: POST /warehouse-inventory/bulk-mark-inspected reuses WarehouseInspectionService.create
- EXPORT items advance to READY_FOR_LOADING after inspection PASSED
- InventoryWorkbench: selection state + "Mark Selected as Inspected" bulk button
- WarehouseInventoryTable: optional Checkbox column for bulk selection
- Route-based direction in eligibleBookings, bulkReceive, getBookingDirection
- New BulkInspectDto; service + controller wired

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hagernesh
2026-06-20 17:52:01 +00:00
parent b7a831b063
commit 50c6381341
10 changed files with 220 additions and 12 deletions

View File

@@ -0,0 +1,26 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ArrayNotEmpty, IsArray, IsOptional, IsString, IsUUID } from 'class-validator';
/** Bulk-mark received inventory items as inspection PASSED. */
export class BulkInspectDto {
@ApiProperty({ type: [String], format: 'uuid' })
@IsArray()
@ArrayNotEmpty()
@IsUUID('all', { each: true })
inventoryIds!: string[];
@ApiPropertyOptional({ description: 'Inspection type label (e.g. ORIGIN_INSPECTION).' })
@IsOptional()
@IsString()
inspectionType?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
remarks?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
inspectedBy?: string;
}

View File

@@ -2,6 +2,7 @@ import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post, Query } from
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { BulkReceiveDto } from './dto/bulk-receive.dto';
import { BulkInspectDto } from './dto/bulk-inspect.dto';
import { DeliverInventoryDto } from './dto/deliver-inventory.dto';
import { FilterWarehouseInventoryDto } from './dto/filter-inventory.dto';
import { InquiryWarehouseInventoryDto } from './dto/inquiry-inventory.dto';
@@ -77,6 +78,12 @@ export class WarehouseInventoryController {
return this.inventoryService.loadPassedExport(performedBy);
}
@Post('bulk-mark-inspected')
@ApiOperation({ summary: 'Bulk mark received inventory inspection PASSED (EXPORT → READY_FOR_LOADING)' })
bulkMarkInspected(@Body() dto: BulkInspectDto) {
return this.inventoryService.bulkMarkInspected(dto);
}
@Post('bookings/:bookingId/unload')
@ApiOperation({ summary: 'Unload a single arrived booking into a location' })
unloadBooking(

View File

@@ -3,6 +3,7 @@ import { DataSource, EntityManager, FindManyOptions, ILike } from 'typeorm';
import { deriveTradeDirection } from '../../common/derive-trade-direction.util';
import { Cargo } from '../cargoes/entities/cargoes.entity';
import { BulkInspectDto } from './dto/bulk-inspect.dto';
import { BulkReceiveDto } from './dto/bulk-receive.dto';
import { DeliverInventoryDto } from './dto/deliver-inventory.dto';
import { FilterWarehouseInventoryDto } from './dto/filter-inventory.dto';
@@ -14,6 +15,7 @@ import { ReleaseOrderDto } from './dto/release-order.dto';
import { ReserveInventoryDto } from './dto/reserve-inventory.dto';
import { UnloadBookingDto } from './dto/unload-booking.dto';
import { WarehouseAllocationService } from './warehouse-allocation.service';
import { WarehouseInspectionService } from './warehouse-inspection.service';
import { WarehouseInvoiceService } from './warehouse-invoice.service';
import { WarehouseActivityLog } from './entities/warehouse-activity-log.entity';
import { WarehouseInventoryMovement } from './entities/warehouse-inventory-movement.entity';
@@ -146,6 +148,12 @@ export interface LoadPassedExportResult {
results: { inventoryId: string; status: string; reason?: string }[];
}
export interface BulkInspectResult {
inspectedCount: number;
skippedCount: number;
results: { inventoryId: string; status: string; reason?: string }[];
}
@Injectable()
export class WarehouseInventoryService {
constructor(
@@ -156,6 +164,7 @@ export class WarehouseInventoryService {
private readonly scheduling: SchedulingReadFacade,
private readonly allocation: WarehouseAllocationService,
private readonly invoices: WarehouseInvoiceService,
private readonly inspectionService: WarehouseInspectionService,
) {}
/**
@@ -592,6 +601,63 @@ export class WarehouseInventoryService {
return result;
}
/**
* Bulk-mark received items inspection PASSED (reusing the inspection service to create a minimal
* report + sync inspectionStatus/inspectedAt). EXPORT items advance straight to READY_FOR_LOADING.
* For damage / weight-loss / images, use the per-item Inspect / Report action instead.
*/
async bulkMarkInspected(dto: BulkInspectDto): Promise<BulkInspectResult> {
const result: BulkInspectResult = { inspectedCount: 0, skippedCount: 0, results: [] };
const eligible = ['RECEIVED', 'STORED', 'RESERVED'];
for (const inventoryId of dto.inventoryIds) {
const skip = (reason: string) => {
result.skippedCount += 1;
result.results.push({ inventoryId, status: 'SKIPPED', reason });
};
const item = await this.inventoryRepository.findById(inventoryId);
if (!item) { skip('Inventory not found'); continue; }
if (item.inspectionStatus === 'PASSED') { skip('Already inspected'); continue; }
if (!eligible.includes(item.status)) { skip(`Status ${item.status} not eligible for inspection`); continue; }
// Reuse the existing inspection service: creates a minimal PASSED report + sets inspectionStatus/inspectedAt.
await this.inspectionService.create(inventoryId, {
reportType: 'INSPECTION',
inspectionStatus: 'PASSED',
remarks: dto.remarks ?? 'Bulk marked inspected (PASSED).',
inspectedById: dto.inspectedBy,
});
// EXPORT: a passed item moves straight to Ready To Load.
const direction = item.bookingId ? await this.getBookingDirection(item.bookingId) : null;
if (direction === 'EXPORT') {
await this.dataSource.transaction(async (manager) => {
await manager.getRepository(WarehouseInventory).update(inventoryId, {
status: 'READY_FOR_LOADING',
readyForLoadingAt: new Date(),
});
await this.activityLog.record(
{
activityType: 'READY_FOR_LOADING',
inventoryId,
warehouseId: item.warehouseId,
description: 'Inspection passed → ready for loading',
performedBy: dto.inspectedBy,
},
manager,
);
});
result.results.push({ inventoryId, status: 'READY_FOR_LOADING' });
} else {
result.results.push({ inventoryId, status: 'INSPECTED' });
}
result.inspectedCount += 1;
}
return result;
}
// ── Receive ──────────────────────────────────────────────────────────────
async receive(dto: ReceiveWarehouseInventoryDto): Promise<WarehouseInventory> {