diff --git a/apps/edr-freight-api/src/migrations/2470000000000-AddAcquisitionItemName.ts b/apps/edr-freight-api/src/migrations/2470000000000-AddAcquisitionItemName.ts new file mode 100644 index 000000000..fadacda69 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/2470000000000-AddAcquisitionItemName.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * Acquisitions describe WHAT was acquired (vehicle, parts, equipment…) — the + * vehicle link is optional and only for acquisitions that ARE a fleet vehicle. + */ +export class AddAcquisitionItemName2470000000000 implements MigrationInterface { + name = 'AddAcquisitionItemName2470000000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.asset_acquisitions + ADD COLUMN IF NOT EXISTS item_name varchar(200) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.asset_acquisitions + DROP COLUMN IF EXISTS item_name + `); + } +} diff --git a/apps/edr-freight-api/src/modules/procurement/dto/procurement.dto.ts b/apps/edr-freight-api/src/modules/procurement/dto/procurement.dto.ts index 943d79296..cfab53a5d 100644 --- a/apps/edr-freight-api/src/modules/procurement/dto/procurement.dto.ts +++ b/apps/edr-freight-api/src/modules/procurement/dto/procurement.dto.ts @@ -7,6 +7,7 @@ import { IsOptional, IsEnum, IsBoolean, + MinLength, } from 'class-validator'; import { VendorType } from '../entities/vendor.entity'; import { AcquisitionType, AcquisitionStatus } from '../entities/asset-acquisition.entity'; @@ -72,6 +73,11 @@ export class UpdateVendorDto { } export class CreateAcquisitionDto { + /** WHAT was acquired — required so an acquisition can't be saved empty. */ + @IsString() + @MinLength(2) + itemName!: string; + @IsOptional() @IsUUID() vehicleId?: string; @@ -120,6 +126,11 @@ export class CreateAcquisitionDto { } export class UpdateAcquisitionDto { + @IsOptional() + @IsString() + @MinLength(2) + itemName?: string; + @IsOptional() @IsUUID() vehicleId?: string; diff --git a/apps/edr-freight-api/src/modules/procurement/entities/asset-acquisition.entity.ts b/apps/edr-freight-api/src/modules/procurement/entities/asset-acquisition.entity.ts index d4f781c15..e1a9f4ff5 100644 --- a/apps/edr-freight-api/src/modules/procurement/entities/asset-acquisition.entity.ts +++ b/apps/edr-freight-api/src/modules/procurement/entities/asset-acquisition.entity.ts @@ -18,6 +18,12 @@ export enum AcquisitionStatus { @Entity({ name: 'asset_acquisitions', schema: 'freight' }) @Index(['vehicleId', 'acquisitionDate']) export class AssetAcquisition extends BaseEntity { + /** WHAT was acquired (vehicle, parts, equipment…) — the asset itself. */ + @Column({ name: 'item_name', type: 'varchar', length: 200, nullable: true }) + itemName?: string; + + /** Optional link — only when the acquisition IS a fleet vehicle. Parts and + * general procurement stay unlinked so reports don't misattribute them. */ @Column({ name: 'vehicle_id', type: 'uuid', nullable: true }) vehicleId?: string; diff --git a/apps/edr-freight-api/src/modules/procurement/procurement.acquisition-guard.spec.ts b/apps/edr-freight-api/src/modules/procurement/procurement.acquisition-guard.spec.ts new file mode 100644 index 000000000..8004d9d9e --- /dev/null +++ b/apps/edr-freight-api/src/modules/procurement/procurement.acquisition-guard.spec.ts @@ -0,0 +1,50 @@ +import { BadRequestException } from '@nestjs/common'; + +import { ProcurementService } from './procurement.service'; +import { AcquisitionType } from './entities/asset-acquisition.entity'; + +// PURCHASE acquisitions must not carry lease terms; LEASE/RENTAL may. +describe('ProcurementService acquisition lease-field guard', () => { + const repo = { + createAcquisition: jest.fn(async (dto) => dto), + findAcquisitionById: jest.fn(async () => ({ acquisitionType: AcquisitionType.PURCHASE })), + updateAcquisition: jest.fn(async (_id, dto) => dto), + }; + const svc = new ProcurementService(repo as never); + + it('rejects a PURCHASE with lease dates', async () => { + await expect( + svc.createAcquisition({ + itemName: 'Brake pads', + acquisitionType: AcquisitionType.PURCHASE, + acquisitionDate: '2026-07-22', + leaseStart: '2026-07-01', + } as never), + ).rejects.toThrow(BadRequestException); + }); + + it('accepts a LEASE with lease dates and a plain PURCHASE', async () => { + await expect( + svc.createAcquisition({ + itemName: 'Rented crane', + acquisitionType: AcquisitionType.LEASE, + acquisitionDate: '2026-07-22', + leaseStart: '2026-07-01', + leaseEnd: '2027-07-01', + } as never), + ).resolves.toBeDefined(); + await expect( + svc.createAcquisition({ + itemName: 'Brake pads', + acquisitionType: AcquisitionType.PURCHASE, + acquisitionDate: '2026-07-22', + } as never), + ).resolves.toBeDefined(); + }); + + it('rejects adding lease terms to an acquisition that is a PURCHASE', async () => { + await expect( + svc.updateAcquisition('a1', { monthlyPayment: 500 } as never), + ).rejects.toThrow(BadRequestException); + }); +}); diff --git a/apps/edr-freight-api/src/modules/procurement/procurement.service.ts b/apps/edr-freight-api/src/modules/procurement/procurement.service.ts index e799d5ff9..7095a6a09 100644 --- a/apps/edr-freight-api/src/modules/procurement/procurement.service.ts +++ b/apps/edr-freight-api/src/modules/procurement/procurement.service.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { ProcurementRepository } from './procurement.repository'; import { Vendor } from './entities/vendor.entity'; -import { AssetAcquisition } from './entities/asset-acquisition.entity'; +import { AcquisitionType, AssetAcquisition } from './entities/asset-acquisition.entity'; import { AssetDisposal } from './entities/asset-disposal.entity'; import { CreateVendorDto, @@ -51,7 +51,23 @@ export class ProcurementService { } // ---- Acquisitions ---- + /** Lease terms only make sense on LEASE / RENTAL — a PURCHASE must not carry them. */ + private assertLeaseFieldsValid(dto: { + acquisitionType?: string; + leaseStart?: string; + leaseEnd?: string; + monthlyPayment?: number; + }): void { + if (dto.acquisitionType !== AcquisitionType.PURCHASE) return; + if (dto.leaseStart || dto.leaseEnd || dto.monthlyPayment != null) { + throw new BadRequestException( + 'Lease start/end and monthly payment are not valid for a PURCHASE acquisition', + ); + } + } + async createAcquisition(dto: CreateAcquisitionDto): Promise { + this.assertLeaseFieldsValid(dto); return this.procurementRepository.createAcquisition(dto); } @@ -64,6 +80,20 @@ export class ProcurementService { } async updateAcquisition(id: string, dto: UpdateAcquisitionDto): Promise { + // Validate against the resulting record, not just the patch — switching an + // acquisition to PURCHASE must also shed any stored lease terms. + const existing = await this.procurementRepository.findAcquisitionById(id); + if (existing) { + const next = { ...existing, ...dto }; + if (next.acquisitionType === AcquisitionType.PURCHASE) { + this.assertLeaseFieldsValid({ + acquisitionType: next.acquisitionType, + leaseStart: dto.leaseStart, + leaseEnd: dto.leaseEnd, + monthlyPayment: dto.monthlyPayment, + }); + } + } return this.procurementRepository.updateAcquisition(id, dto); } diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts index bd702542b..8e9e0bc76 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts @@ -1578,6 +1578,14 @@ export class WarehouseInventoryService { notes: `Bulk received (${dto.direction})`, truckEntrance, }); + + // Validate capacity before saving + const weight = Number(booking.weight) || 0; + const containerCount = booking.freightType === 'CONTAINER' ? containerQuantity : 0; + this.assertCapacity('Warehouse', warehouse, weight, 0, containerCount); + this.assertCapacity('Yard', yard, weight, 0, containerCount); + this.assertCapacity('Zone', zone, weight, 0, containerCount); + const saved = await manager.getRepository(WarehouseInventory).save( manager.getRepository(WarehouseInventory).create({ warehouseId: dto.warehouseId, @@ -1585,7 +1593,7 @@ export class WarehouseInventoryService { zoneId: dto.zoneId, bookingId, quantity: booking.freightType === 'CONTAINER' ? containerQuantity : 1, - weight: Number(booking.weight) || 0, + weight, grnNumber, status: 'RECEIVED', arrivedAt: now, @@ -1593,6 +1601,9 @@ export class WarehouseInventoryService { }), ); + // Update warehouse/yard/zone capacity counters + await this.applyCapacityDelta(manager, dto, weight, 0, containerCount); + // Receiving the booking flags every container unit as received into the // port (self-haul export: the delivering truck's goods are now in) so // staff can raise the per-container GRN over what's received. diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-yards.service.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-yards.service.ts index 3279e9092..5b5e2b227 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-yards.service.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-yards.service.ts @@ -1,4 +1,4 @@ -import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'; +import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common'; import { CreateWarehouseYardDto } from './dto/create-warehouse-yard.dto'; import { UpdateWarehouseYardDto } from './dto/update-warehouse-yard.dto'; @@ -44,6 +44,7 @@ export class WarehouseYardsService { // Ensure the parent warehouse exists. await this.warehousesService.findById(warehouseId); await this.assertCodeUnique(warehouseId, dto.code.trim()); + await this.assertCapacityWithinWarehouse(warehouseId, dto.capacityWeight ?? null, dto.capacityContainers ?? null); return this.yardsRepository.create({ warehouseId, @@ -69,14 +70,22 @@ export class WarehouseYardsService { await this.assertCodeUnique(existing.warehouseId, dto.code.trim(), id); } + const newCapacityWeight = dto.capacityWeight ?? existing.capacityWeight ?? null; + const newCapacityContainers = dto.capacityContainers ?? existing.capacityContainers ?? null; + + // Validate updated capacity doesn't exceed warehouse limits + if (newCapacityWeight !== (existing.capacityWeight ?? null) || newCapacityContainers !== (existing.capacityContainers ?? null)) { + await this.assertCapacityWithinWarehouse(existing.warehouseId, newCapacityWeight, newCapacityContainers, id); + } + const status = dto.status ?? existing.status; const updated = await this.yardsRepository.update(id, { name: dto.name?.trim() ?? existing.name, code: dto.code?.trim() ?? existing.code, type: dto.type ?? existing.type, - capacityWeight: dto.capacityWeight ?? existing.capacityWeight, - capacityContainers: dto.capacityContainers ?? existing.capacityContainers, + capacityWeight: newCapacityWeight, + capacityContainers: newCapacityContainers, maxWeight: dto.maxWeight ?? existing.maxWeight, maxVolume: dto.maxVolume ?? existing.maxVolume, status, @@ -97,4 +106,39 @@ export class WarehouseYardsService { throw new ConflictException(`Yard code ${code} already exists in this warehouse`); } } + + private async assertCapacityWithinWarehouse( + warehouseId: string, + newCapacityWeight: number | null, + newCapacityContainers: number | null, + excludeYardId?: string, + ): Promise { + const warehouse = await this.warehousesService.findById(warehouseId); + const yards = await this.findByWarehouse(warehouseId); + + // Sum existing yard capacities, excluding the yard being updated if provided + const otherYards = excludeYardId ? yards.filter((y) => y.id !== excludeYardId) : yards; + const totalExistingWeight = otherYards.reduce((sum, y) => sum + (y.capacityWeight ?? 0), 0); + const totalExistingContainers = otherYards.reduce((sum, y) => sum + (y.capacityContainers ?? 0), 0); + + // Check weight capacity + if (newCapacityWeight !== null && warehouse.capacityWeight != null) { + const totalWeight = totalExistingWeight + newCapacityWeight; + if (totalWeight > warehouse.capacityWeight) { + throw new BadRequestException( + `Total yard weight capacity (${totalWeight}t) exceeds warehouse limit (${warehouse.capacityWeight}t)`, + ); + } + } + + // Check container capacity + if (newCapacityContainers !== null && warehouse.capacityContainers != null) { + const totalContainers = totalExistingContainers + newCapacityContainers; + if (totalContainers > warehouse.capacityContainers) { + throw new BadRequestException( + `Total yard container capacity (${totalContainers}) exceeds warehouse limit (${warehouse.capacityContainers})`, + ); + } + } + } } diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-zones.service.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-zones.service.ts index b4ae2e0de..367a5a75e 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-zones.service.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-zones.service.ts @@ -1,4 +1,4 @@ -import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'; +import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common'; import { CreateWarehouseZoneDto } from './dto/create-warehouse-zone.dto'; import { UpdateWarehouseZoneDto } from './dto/update-warehouse-zone.dto'; @@ -43,6 +43,7 @@ export class WarehouseZonesService { // Ensure the parent yard exists. await this.yardsService.findById(yardId); await this.assertCodeUnique(yardId, dto.code.trim()); + await this.assertCapacityWithinYard(yardId, dto.capacityWeight ?? null, dto.capacityContainers ?? null); return this.zonesRepository.create({ yardId, @@ -68,14 +69,22 @@ export class WarehouseZonesService { await this.assertCodeUnique(existing.yardId, dto.code.trim(), id); } + const newCapacityWeight = dto.capacityWeight ?? existing.capacityWeight ?? null; + const newCapacityContainers = dto.capacityContainers ?? existing.capacityContainers ?? null; + + // Validate updated capacity doesn't exceed yard limits + if (newCapacityWeight !== (existing.capacityWeight ?? null) || newCapacityContainers !== (existing.capacityContainers ?? null)) { + await this.assertCapacityWithinYard(existing.yardId, newCapacityWeight, newCapacityContainers, id); + } + const status = dto.status ?? existing.status; const updated = await this.zonesRepository.update(id, { name: dto.name?.trim() ?? existing.name, code: dto.code?.trim() ?? existing.code, type: dto.type ?? existing.type, - capacityWeight: dto.capacityWeight ?? existing.capacityWeight, - capacityContainers: dto.capacityContainers ?? existing.capacityContainers, + capacityWeight: newCapacityWeight, + capacityContainers: newCapacityContainers, maxWeight: dto.maxWeight ?? existing.maxWeight, maxVolume: dto.maxVolume ?? existing.maxVolume, status, @@ -96,4 +105,39 @@ export class WarehouseZonesService { throw new ConflictException(`Zone code ${code} already exists in this yard`); } } + + private async assertCapacityWithinYard( + yardId: string, + newCapacityWeight: number | null, + newCapacityContainers: number | null, + excludeZoneId?: string, + ): Promise { + const yard = await this.yardsService.findById(yardId); + const zones = await this.findByYard(yardId); + + // Sum existing zone capacities, excluding the zone being updated if provided + const otherZones = excludeZoneId ? zones.filter((z) => z.id !== excludeZoneId) : zones; + const totalExistingWeight = otherZones.reduce((sum, z) => sum + (z.capacityWeight ?? 0), 0); + const totalExistingContainers = otherZones.reduce((sum, z) => sum + (z.capacityContainers ?? 0), 0); + + // Check weight capacity + if (newCapacityWeight !== null && yard.capacityWeight != null) { + const totalWeight = totalExistingWeight + newCapacityWeight; + if (totalWeight > yard.capacityWeight) { + throw new BadRequestException( + `Total zone weight capacity (${totalWeight}t) exceeds yard limit (${yard.capacityWeight}t)`, + ); + } + } + + // Check container capacity + if (newCapacityContainers !== null && yard.capacityContainers != null) { + const totalContainers = totalExistingContainers + newCapacityContainers; + if (totalContainers > yard.capacityContainers) { + throw new BadRequestException( + `Total zone container capacity (${totalContainers}) exceeds yard limit (${yard.capacityContainers})`, + ); + } + } + } } diff --git a/apps/edr-freight-api/src/seed/freight-permissions.registry.ts b/apps/edr-freight-api/src/seed/freight-permissions.registry.ts index 1f82f6e93..741ceafb1 100644 --- a/apps/edr-freight-api/src/seed/freight-permissions.registry.ts +++ b/apps/edr-freight-api/src/seed/freight-permissions.registry.ts @@ -808,41 +808,41 @@ export const POSITION_PERMISSION_PRESETS = { // permission catalog (all CRUD across bookings, contracts, scheduling, // fleet, warehouse, mile, finance, settings, staff). operationsChief: dedupe([...BOOKING_RULE_ENGINE_PERMISSION_KEYS]), - // Dispatcher: warehouse floor operations — receive/GRN, move, load/unload, - // inspect, dispatch, gate, release/deliver, interchange docs, fee invoices, - // plus truck dispatch on the mile legs and read-only operational context. - // Allocation & fee rules are VIEW-ONLY — never create/update/delete. + // Dispatcher: full CRUD on warehouse management (incl. import/export/intercity + // inventory flows) and fleet management, plus truck dispatch on the mile legs + // and operational context. The ONE carve-out: allocation & fee rules stay + // VIEW-ONLY — a dispatcher never creates/updates/deletes those rules. dispatcher: dedupe([ + // Warehouse management — full CRUD. FREIGHT_PERMS.warehouseDashboard.view, - FREIGHT_PERMS.warehouses.view, - FREIGHT_PERMS.warehouseYards.view, - FREIGHT_PERMS.warehouseZones.view, - FREIGHT_PERMS.warehouseInventory.view, - FREIGHT_PERMS.warehouseInventory.receive, - FREIGHT_PERMS.warehouseInventory.move, - FREIGHT_PERMS.warehouseInventory.load, - FREIGHT_PERMS.warehouseInventory.unload, - FREIGHT_PERMS.warehouseInventory.dispatch, - FREIGHT_PERMS.warehouseInventory.gatePass, - FREIGHT_PERMS.warehouseInventory.release, - FREIGHT_PERMS.warehouseInventory.deliver, - FREIGHT_PERMS.warehouseInventory.inspect, - FREIGHT_PERMS.warehouseInspectionReports.view, - FREIGHT_PERMS.warehouseInspectionReports.create, - FREIGHT_PERMS.warehouseInspectionReports.update, - FREIGHT_PERMS.interchangeDocuments.view, - FREIGHT_PERMS.interchangeDocuments.generate, - FREIGHT_PERMS.interchangeDocuments.acknowledge, - FREIGHT_PERMS.warehouseFeeInvoices.view, - FREIGHT_PERMS.warehouseFeeInvoices.generate, + ...Object.values(FREIGHT_PERMS.warehouses), + ...Object.values(FREIGHT_PERMS.warehouseYards), + ...Object.values(FREIGHT_PERMS.warehouseZones), + ...Object.values(FREIGHT_PERMS.warehouseInventory), + ...Object.values(FREIGHT_PERMS.warehouseInspectionReports), + ...Object.values(FREIGHT_PERMS.interchangeDocuments), + ...Object.values(FREIGHT_PERMS.warehouseFeeInvoices), // View-only on the rules that govern allocation and fees. FREIGHT_PERMS.warehouseAllocationRules.view, FREIGHT_PERMS.warehouseFeeRules.view, + // Fleet management — full CRUD. + ...Object.values(FREIGHT_PERMS.fleet), + FREIGHT_PERMS.fleetDashboard.view, + ...Object.values(FREIGHT_PERMS.fleetReports), + ...Object.values(FREIGHT_PERMS.vehicles), + ...Object.values(FREIGHT_PERMS.drivers), + ...Object.values(FREIGHT_PERMS.tracking), + ...Object.values(FREIGHT_PERMS.fuel), + ...Object.values(FREIGHT_PERMS.maintenance), + ...Object.values(FREIGHT_PERMS.locomotives), + ...Object.values(FREIGHT_PERMS.wagons), + ...Object.values(FREIGHT_PERMS.trains), + ...Object.values(FREIGHT_PERMS.routes), + ...Object.values(FREIGHT_PERMS.containers), + ...Object.values(FREIGHT_PERMS.cargoes), // Truck dispatch on the EDR mile legs + operational context. - FREIGHT_PERMS.firstMile.view, - FREIGHT_PERMS.firstMile.assignVehicles, - FREIGHT_PERMS.lastMile.view, - FREIGHT_PERMS.lastMile.assignVehicles, + ...Object.values(FREIGHT_PERMS.firstMile), + ...Object.values(FREIGHT_PERMS.lastMile), FREIGHT_PERMS.trainScheduling.view, FREIGHT_PERMS.bookings.operations, ]), diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/ProcurementPage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/ProcurementPage.tsx index 0c6bbabf0..584e3e13b 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/ProcurementPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/ProcurementPage.tsx @@ -61,6 +61,7 @@ const clean = >(obj: T): Partial => ) as Partial; const emptyAcquisition = { + itemName: "", vehicleId: "", vendorId: "", acquisitionType: "PURCHASE" as AcquisitionType, @@ -239,6 +240,7 @@ export default function ProcurementPage() { + Item / Asset Vehicle Type Date @@ -249,7 +251,7 @@ export default function ProcurementPage() { {loadingAcquisitions ? ( - + @@ -257,7 +259,7 @@ export default function ProcurementPage() { ) : acquisitions.length === 0 ? ( - + No acquisitions recorded yet. @@ -266,6 +268,7 @@ export default function ProcurementPage() { ) : null} {acquisitions.map((a: AssetAcquisition) => ( + {a.itemName || "—"} {vehicleLabel(a.vehicle, a.vehicleId)} @@ -411,31 +414,51 @@ export default function ProcurementPage() { size="lg" > + setAcqForm({ ...acqForm, itemName: e.currentTarget.value })} + required + /> setAcqForm({ ...acqForm, vendorId: val || "" })} - searchable - clearable - /> + + - setAcqForm({ ...acqForm, acquisitionType: (val as AcquisitionType) || "PURCHASE" }) - } + onChange={(val) => { + const acquisitionType = (val as AcquisitionType) || "PURCHASE"; + // Lease terms are invalid on a purchase — drop them on switch. + setAcqForm( + acquisitionType === "PURCHASE" + ? { ...acqForm, acquisitionType, leaseStart: "", leaseEnd: "", monthlyPayment: undefined } + : { ...acqForm, acquisitionType }, + ); + }} required /> - setAcqForm({ ...acqForm, leaseStart: e.currentTarget.value })} - /> - setAcqForm({ ...acqForm, leaseEnd: e.currentTarget.value })} - /> - - setAcqForm({ ...acqForm, monthlyPayment: val as number | undefined }) - } - decimalScale={2} - min={0} - /> + {acqForm.acquisitionType !== "PURCHASE" && ( + <> + setAcqForm({ ...acqForm, leaseStart: e.currentTarget.value })} + /> + setAcqForm({ ...acqForm, leaseEnd: e.currentTarget.value })} + /> + + setAcqForm({ ...acqForm, monthlyPayment: val as number | undefined }) + } + decimalScale={2} + min={0} + /> + + )}