Pond Creation Simplification Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Remove the code field from Pond entirely, make surfaceArea/depth/type optional so a pond can be created with just an area and a name, and simplify PondForm with progressive disclosure.

Architecture: Backend contract changes first (migration, entity, DTOs, service logic, all response mappings that embed pond info), then frontend type/schema changes, then frontend component edits. This order matters because the frontend types must match the backend contract before the components that consume them can compile.

Tech Stack: NestJS 11 + TypeORM 0.3 + PostgreSQL (backend), React 19 + Ant Design 6 + TanStack Query + Zod v4 (frontend), Jest (backend tests), Vitest (frontend tests), Biome (lint/format).

Spec: docs/superpowers/specs/2026-07-05-pond-creation-simplification-design.md


Before you start

Run these once to confirm a clean baseline (from repo root):

cd backend && bun run test 2>&1 | tail -20
cd ../frontend && bun run test 2>&1 | tail -20

Both should be green before Task 1. All commands below assume you’re in the repo root unless a step says cd backend / cd frontend.


Backend

Task 1: Migration — drop code, relax surface_area/type nullability

Files:

  • Create: backend/src/migrations/1779200000000-SimplifyPondCreationFields.ts

  • Step 1: Write the migration

import { MigrationInterface, QueryRunner } from 'typeorm';

export class SimplifyPondCreationFields1779200000000 implements MigrationInterface {
  name = 'SimplifyPondCreationFields1779200000000';

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`ALTER TABLE "ponds" DROP COLUMN "code"`);
    await queryRunner.query(`ALTER TABLE "ponds" ALTER COLUMN "surface_area" DROP NOT NULL`);
    await queryRunner.query(`ALTER TABLE "ponds" ALTER COLUMN "type" DROP NOT NULL`);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`ALTER TABLE "ponds" ALTER COLUMN "type" SET NOT NULL`);
    await queryRunner.query(`ALTER TABLE "ponds" ALTER COLUMN "surface_area" SET NOT NULL`);
    await queryRunner.query(`ALTER TABLE "ponds" ADD COLUMN "code" character varying(50)`);
  }
}

Note: the down() migration is best-effort — if any pond rows already have a null
surface_area/type by the time you roll back, SET NOT NULL will fail. This mirrors
the accepted risk in the spec (dropping code is a one-way data loss already).

  • Step 2: Run the migration against your local dev DB
cd backend
bun run migration:run

Expected: output includes Migration SimplifyPondCreationFields1779200000000 has been executed successfully.

  • Step 3: Commit
git add backend/src/migrations/1779200000000-SimplifyPondCreationFields.ts
git commit -m "feat(farm): add migration dropping pond code and relaxing surfaceArea/type nullability"

Task 2: Update the Pond entity

Files:

  • Modify: backend/src/farm/entities/pond.entity.ts

  • Step 1: Remove the code column and relax surfaceArea/type

Replace the whole file with:

import {
  Column,
  CreateDateColumn,
  Entity,
  JoinColumn,
  JoinTable,
  ManyToMany,
  ManyToOne,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';
import { User } from '@/auth/entities/user.entity';
import { Device } from '@/devices/entities/device.entity';
import { Organization } from '@/organizations/entities/organization.entity';
import { Area } from './area.entity';

@Entity('ponds')
export class Pond {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ name: 'area_id' })
  areaId: string;

  @Column({ name: 'organization_id' })
  organizationId: string;

  @Column({ length: 255 })
  name: string;

  @Column({ name: 'surface_area', type: 'decimal', precision: 10, scale: 2, nullable: true })
  surfaceArea: number | null;

  @Column({ type: 'decimal', precision: 5, scale: 2, nullable: true })
  depth: number | null;

  @Column({ length: 20, nullable: true })
  type: string | null;

  @Column({
    type: 'enum',
    enum: ['active', 'inactive', 'maintenance'],
    default: 'active',
  })
  status: string;

  @Column({ name: 'current_cycle_id', type: 'uuid', nullable: true })
  currentCycleId: string | null;

  @CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
  createdAt: Date;

  @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
  updatedAt: Date;

  @ManyToOne(() => Area)
  @JoinColumn({ name: 'area_id' })
  area: Area;

  @ManyToOne(() => Organization)
  @JoinColumn({ name: 'organization_id' })
  organization: Organization;

  @ManyToMany(
    () => Device,
    (device) => device.ponds,
  )
  @JoinTable({
    name: 'pond_devices',
    joinColumn: { name: 'pond_id', referencedColumnName: 'id' },
    inverseJoinColumn: { name: 'device_id', referencedColumnName: 'id' },
  })
  devices: Device[];

  @ManyToMany(() => User)
  @JoinTable({
    name: 'pond_users',
    joinColumn: { name: 'pond_id', referencedColumnName: 'id' },
    inverseJoinColumn: { name: 'user_id', referencedColumnName: 'id' },
  })
  assignedUsers: User[];
}

This step alone will break compilation across many files that reference Pond.code
that’s expected. Each of the following tasks fixes one cluster of those breaks. Don’t
run the full build yet; each task below runs its own scoped check.

  • Step 2: Commit
git add backend/src/farm/entities/pond.entity.ts
git commit -m "feat(farm): remove code column, relax surfaceArea/type nullability on Pond entity"

Task 3: Update CreatePondDto, add “minimal pond” test

Files:

  • Modify: backend/src/farm/dto/create-pond.dto.ts

  • Test: backend/src/farm/ponds.service.spec.ts

  • Step 1: Write the failing test — add a new test inside the existing
    describe('create', ...) block in backend/src/farm/ponds.service.spec.ts, right after
    the 'should create pond successfully' test (after its closing });, before the
    'should throw BadRequestException for duplicate code' test:

    it('should create pond successfully with only areaId and name', async () => {
      const createDto: CreatePondDto = {
        areaId: 'area-id-1',
        name: 'Ao tối giản',
      };

      areaRepository.findOne = jest.fn().mockResolvedValue(mockArea);
      const createdPond = {
        ...mockPond,
        name: createDto.name,
        areaId: createDto.areaId,
        surfaceArea: null,
        depth: null,
        type: null,
      } as unknown as PondEntity;

      dataSource.transaction = jest.fn().mockImplementation(async (callback) => {
        const manager = {
          create: jest.fn().mockReturnValue(createdPond),
          save: jest.fn().mockResolvedValue(createdPond),
        };
        return await callback(manager);
      });
      const result = await service.create(createDto, mockCurrentUser);

      expect(result.name).toBe('Ao tối giản');
      expect(result.surfaceArea).toBeUndefined();
    });
  • Step 2: Run the test to verify it fails
cd backend
npx jest ponds.service.spec.ts -t "only areaId and name"

Expected: FAIL — TypeScript error, since CreatePondDto still requires surfaceArea
and type and the object literal { areaId, name } doesn’t satisfy it.

  • Step 3: Update CreatePondDto — replace the whole file:
import { ApiProperty } from '@nestjs/swagger';
import {
  IsEnum,
  IsNotEmpty,
  IsNumber,
  IsOptional,
  IsString,
  IsUUID,
  Max,
  Min,
  MinLength,
} from 'class-validator';

export class CreatePondDto {
  @ApiProperty({ example: 'uuid' })
  @IsUUID()
  @IsNotEmpty({ message: 'Area ID is required' })
  areaId: string;

  @ApiProperty({ example: 'Ao 1' })
  @IsString()
  @IsNotEmpty({ message: 'Pond name is required' })
  @MinLength(2, { message: 'Pond name must be at least 2 characters' })
  name: string;

  @ApiProperty({
    example: 1000,
    description: 'Surface area in square meters',
    required: false,
  })
  @IsOptional()
  @IsNumber()
  @Min(1, { message: 'Surface area must be at least 1' })
  surfaceArea?: number;

  @ApiProperty({ example: 2.5, description: 'Average depth in meters', required: false })
  @IsOptional()
  @IsNumber()
  @Min(0.1, { message: 'Depth must be at least 0.1 meters' })
  @Max(20, { message: 'Depth cannot exceed 20 meters' })
  depth?: number;

  @ApiProperty({ enum: ['lined', 'earthen'], example: 'lined', required: false })
  @IsOptional()
  @IsEnum(['lined', 'earthen'], {
    message: 'Type must be either "lined" or "earthen"',
  })
  type?: 'lined' | 'earthen';

  @ApiProperty({
    enum: ['active', 'inactive', 'maintenance'],
    example: 'active',
    required: false,
  })
  @IsOptional()
  @IsEnum(['active', 'inactive', 'maintenance'], {
    message: 'Status must be active, inactive, or maintenance',
  })
  status?: 'active' | 'inactive' | 'maintenance';
}
  • Step 4: Run the test to verify it passes
cd backend
npx jest ponds.service.spec.ts -t "only areaId and name"

Expected: still FAIL at this point (the service and other tests still reference code)
— that’s fine, this step only confirms the DTO itself now accepts a minimal payload.
Full green comes after Task 4.

  • Step 5: Commit
git add backend/src/farm/dto/create-pond.dto.ts backend/src/farm/ponds.service.spec.ts
git commit -m "feat(farm): remove code and make surfaceArea/type optional on CreatePondDto"

Task 4: Remove code duplicate-check and code wiring from ponds.service.ts

Files:

  • Modify: backend/src/farm/ponds.service.ts

  • Modify: backend/src/farm/ponds.service.spec.ts

  • Modify: backend/src/farm/controllers/ponds.controller.spec.ts

  • Step 1: Remove the unused Not import

In backend/src/farm/ponds.service.ts, change:

import { DataSource, type DeepPartial, Not, type Repository } from 'typeorm';

to:

import { DataSource, type DeepPartial, type Repository } from 'typeorm';
  • Step 2: Remove the duplicate-code check and code field in create()

Change:

    const existingPond = await this.pondRepository.findOne({
      where: {
        code: createPondDto.code,
        areaId: createPondDto.areaId,
      },
    });

    if (existingPond) {
      throw new BadRequestException('Pond code already exists in this area');
    }

    return this.dataSource.transaction(async (manager) => {
      const pondData: DeepPartial<Pond> = {
        areaId: createPondDto.areaId,
        name: createPondDto.name,
        code: createPondDto.code,
        surfaceArea: createPondDto.surfaceArea,
        depth: createPondDto.depth,
        type: createPondDto.type,
        status: createPondDto.status ?? 'active',
        organizationId,
      };

to:

    return this.dataSource.transaction(async (manager) => {
      const pondData: DeepPartial<Pond> = {
        areaId: createPondDto.areaId,
        name: createPondDto.name,
        surfaceArea: createPondDto.surfaceArea,
        depth: createPondDto.depth,
        type: createPondDto.type,
        status: createPondDto.status ?? 'active',
        organizationId,
      };
  • Step 3: Remove the duplicate-code check and code field in update()

Change:

    if (updatePondDto.code && updatePondDto.code !== pond.code) {
      const existingPond = await this.pondRepository.findOne({
        where: {
          code: updatePondDto.code,
          areaId: updatePondDto.areaId ?? pond.areaId,
          id: Not(id),
        },
      });

      if (existingPond) {
        throw new BadRequestException('Pond code already exists in this area');
      }
    }

    Object.assign(pond, {
      areaId: updatePondDto.areaId ?? pond.areaId,
      name: updatePondDto.name ?? pond.name,
      code: updatePondDto.code ?? pond.code,
      surfaceArea: updatePondDto.surfaceArea ?? pond.surfaceArea,
      depth: updatePondDto.depth ?? pond.depth,
      type: updatePondDto.type ?? pond.type,
      status: updatePondDto.status ?? pond.status,
    });

to:

    Object.assign(pond, {
      areaId: updatePondDto.areaId ?? pond.areaId,
      name: updatePondDto.name ?? pond.name,
      surfaceArea: updatePondDto.surfaceArea ?? pond.surfaceArea,
      depth: updatePondDto.depth ?? pond.depth,
      type: updatePondDto.type ?? pond.type,
      status: updatePondDto.status ?? pond.status,
    });
  • Step 4: Update ponds.service.spec.ts fixtures and tests

Remove the code: 'KA-01', line from mockPond (around line 39).

Replace the whole describe('create', ...) block with:

  describe('create', () => {
    it('should create pond successfully', async () => {
      const createDto: CreatePondDto = {
        areaId: 'area-id-1',
        name: 'Ao 2',
        surfaceArea: 1200,
        depth: 3.0,
        type: 'lined',
        status: 'active',
      };

      areaRepository.findOne = jest.fn().mockResolvedValue(mockArea);
      const createdPond = {
        ...mockPond,
        name: createDto.name,
        areaId: createDto.areaId,
        surfaceArea: createDto.surfaceArea,
        depth: createDto.depth,
        type: createDto.type,
        status: createDto.status,
      } as PondEntity;

      dataSource.transaction = jest.fn().mockImplementation(async (callback) => {
        const manager = {
          create: jest.fn().mockReturnValue(createdPond),
          save: jest.fn().mockResolvedValue(createdPond),
        };
        return await callback(manager);
      });
      const result = await service.create(createDto, mockCurrentUser);

      expect(result.name).toBe(createDto.name);
      expect(result.surfaceArea).toBe(createDto.surfaceArea);
    });

    it('should create pond successfully with only areaId and name', async () => {
      const createDto: CreatePondDto = {
        areaId: 'area-id-1',
        name: 'Ao tối giản',
      };

      areaRepository.findOne = jest.fn().mockResolvedValue(mockArea);
      const createdPond = {
        ...mockPond,
        name: createDto.name,
        areaId: createDto.areaId,
        surfaceArea: null,
        depth: null,
        type: null,
      } as unknown as PondEntity;

      dataSource.transaction = jest.fn().mockImplementation(async (callback) => {
        const manager = {
          create: jest.fn().mockReturnValue(createdPond),
          save: jest.fn().mockResolvedValue(createdPond),
        };
        return await callback(manager);
      });
      const result = await service.create(createDto, mockCurrentUser);

      expect(result.name).toBe('Ao tối giản');
      expect(result.surfaceArea).toBeUndefined();
    });
  });

(This drops the old 'should throw BadRequestException for duplicate code' test — that
behavior no longer exists — and folds in the Task 3 test with the code fixture line
already removed from mockPond.)

Replace the whole describe('update', ...) block with:

  describe('update', () => {
    it('should update pond successfully', async () => {
      const updateDto: UpdatePondDto = { name: 'Updated Ao 1' };

      pondRepository.findOne = jest.fn().mockResolvedValue({
        ...mockPond,
        area: mockArea,
      });
      pondRepository.save = jest.fn().mockResolvedValue({
        ...mockPond,
        name: 'Updated Ao 1',
      });
      pondRepository.findOne = jest.fn().mockResolvedValue({
        ...mockPond,
        name: 'Updated Ao 1',
        devices: [],
        assignedUsers: [],
      });

      const result = await service.update('pond-id-1', updateDto, mockCurrentUser);

      expect(authorizationPolicyService.isTenantResourceAccessible).toHaveBeenCalledWith(
        mockCurrentUser,
        expect.objectContaining({ action: 'update', resource: 'pond', resourceId: 'pond-id-1' }),
      );
      expect(result.name).toBe('Updated Ao 1');
    });
  });

(This drops the old 'should throw BadRequestException when updating to duplicate code'
test — that code path no longer exists.)

  • Step 5: Update ponds.controller.spec.ts fixtures

In the describe('create', ...) block, remove the code: 'NEW-01', line from both
createDto object literals (the 'should create a new pond' test and the
'should throw ForbiddenException without permission' test).

  • Step 6: Run the tests to verify they pass
cd backend
npx jest ponds.service.spec.ts ponds.controller.spec.ts

Expected: PASS, all tests green.

  • Step 7: Commit
git add backend/src/farm/ponds.service.ts backend/src/farm/ponds.service.spec.ts backend/src/farm/controllers/ponds.controller.spec.ts
git commit -m "feat(farm): remove pond code duplicate-check and code wiring from create/update"

Task 5: Null-safe response mapping in ponds.service.ts + pond-response.dto.ts

Files:

  • Modify: backend/src/farm/ponds.service.ts

  • Modify: backend/src/farm/dto/pond-response.dto.ts

  • Step 1: Update PondResponseDto

In backend/src/farm/dto/pond-response.dto.ts, change:

  @ApiProperty({ example: 'Ao 1' })
  name: string;

  @ApiProperty({ example: 'KA-01', required: false })
  code?: string;

  @ApiProperty({ example: 1000, description: 'Surface area in square meters' })
  surfaceArea: number;

  @ApiProperty({ example: 2.5, description: 'Average depth in meters', required: false })
  depth?: number;

  @ApiProperty({ enum: ['lined', 'earthen'], example: 'lined' })
  type: 'lined' | 'earthen';

to:

  @ApiProperty({ example: 'Ao 1' })
  name: string;

  @ApiProperty({ example: 1000, description: 'Surface area in square meters', required: false })
  surfaceArea?: number;

  @ApiProperty({ example: 2.5, description: 'Average depth in meters', required: false })
  depth?: number;

  @ApiProperty({ enum: ['lined', 'earthen'], example: 'lined', required: false })
  type?: 'lined' | 'earthen';
  • Step 2: Update toPondResponseDto() in ponds.service.ts

Change:

    return {
      id: pond.id,
      areaId: pond.areaId,
      organizationId: pond.organizationId,
      areaName: pondWithRelations?.area?.name,
      name: pond.name,
      code: pond.code ?? undefined,
      surfaceArea: Number(pond.surfaceArea),
      depth: pond.depth ? Number(pond.depth) : undefined,
      type: pond.type as 'lined' | 'earthen',
      status: pond.status as 'active' | 'inactive' | 'maintenance',

to:

    return {
      id: pond.id,
      areaId: pond.areaId,
      organizationId: pond.organizationId,
      areaName: pondWithRelations?.area?.name,
      name: pond.name,
      surfaceArea: pond.surfaceArea != null ? Number(pond.surfaceArea) : undefined,
      depth: pond.depth ? Number(pond.depth) : undefined,
      type: pond.type ? (pond.type as 'lined' | 'earthen') : undefined,
      status: pond.status as 'active' | 'inactive' | 'maintenance',
  • Step 3: Run the tests
cd backend
npx jest ponds.service.spec.ts ponds.controller.spec.ts

Expected: PASS.

  • Step 4: Commit
git add backend/src/farm/ponds.service.ts backend/src/farm/dto/pond-response.dto.ts
git commit -m "fix(farm): null-safe surfaceArea/type mapping in pond response, drop code field"

Task 6: Remove code from the pond search filter

Files:

  • Modify: backend/src/farm/ponds.service.ts

  • Step 1: Simplify the search filter

In findAll(), change:

    if (search) {
      qb.andWhere('(pond.name ILIKE :search OR pond.code ILIKE :search)', {
        search: `%${search}%`,
      });
    }

to:

    if (search) {
      qb.andWhere('pond.name ILIKE :search', {
        search: `%${search}%`,
      });
    }
  • Step 2: Run the tests
cd backend
npx jest ponds.service.spec.ts

Expected: PASS (the 'should filter by areaId'/'should filter by status' tests don’t
touch search, so this is a safe, low-risk change — no new test needed since there’s no
existing test asserting the two-column OR clause to update).

  • Step 3: Commit
git add backend/src/farm/ponds.service.ts
git commit -m "fix(farm): drop pond code from search filter (field removed)"

Task 7: Remove code from device→pond response mapping

Files:

  • Modify: backend/src/devices/devices.service.ts

  • Modify: backend/src/devices/dto/device-response.dto.ts

  • Modify: backend/src/devices/interfaces/device-response.interface.ts

  • Modify: backend/src/devices/devices.service.spec.ts

  • Step 1: Update the mapping in devices.service.ts

Around line 884-889, change:

      pond: device.pond
        ? {
            name: device.pond.name,
            code: device.pond.code,
          }
        : undefined,

to:

      pond: device.pond
        ? {
            name: device.pond.name,
          }
        : undefined,
  • Step 2: Update DeviceResponseDto

In backend/src/devices/dto/device-response.dto.ts, change:

  @ApiProperty({
    example: { name: 'Ao 1', code: 'KA-01' },
    required: false,
    description: 'Assigned pond info',
  })
  pond?: { name: string; code: string };

to:

  @ApiProperty({
    example: { name: 'Ao 1' },
    required: false,
    description: 'Assigned pond info',
  })
  pond?: { name: string };
  • Step 3: Update DeviceResponse interface

In backend/src/devices/interfaces/device-response.interface.ts, change:

  pond?: { name: string; code: string };

to:

  pond?: { name: string };
  • Step 4: Update the test fixture

In backend/src/devices/devices.service.spec.ts, remove the code: 'P1', line
(around line 53) from the pond object inside buildMockDevice.

  • Step 5: Run the tests
cd backend
npx jest devices.service.spec.ts

Expected: PASS.

  • Step 6: Commit
git add backend/src/devices/devices.service.ts backend/src/devices/dto/device-response.dto.ts backend/src/devices/interfaces/device-response.interface.ts backend/src/devices/devices.service.spec.ts
git commit -m "fix(devices): drop pond code from device response mapping"

Task 8: Guard farming-cycle creation against a missing surfaceArea

Files:

  • Modify: backend/src/aquaculture/cycles.service.ts

  • Test: backend/src/aquaculture/cycles.service.spec.ts

  • Step 1: Write the failing test — add to the describe('create', ...) block in
    backend/src/aquaculture/cycles.service.spec.ts, right after the
    'should create a new cycle' test:

    it('should throw BadRequestException if pond has no surfaceArea set', async () => {
      const createDto = {
        pondId: 'pond-1',
        name: 'Test Cycle',
        speciesId: 'species-1',
        initialStock: 5000,
        seedSource: {
          hatchery: 'Test Hatchery',
          strain: 'Test Strain',
          postLarvalStage: 'PL10',
        },
        stockingDate: new Date().toISOString().split('T')[0],
      };

      pondRepository.findOne.mockResolvedValueOnce({ ...mockPond, surfaceArea: null });
      cyclesRepository.findOne.mockResolvedValueOnce(null);

      const currentUser: CurrentUserData = {
        organizationId: 'org-1',
        userId: 'user-1',
        email: 'user@example.com',
        roles: ['Admin'],
        permissions: [],
        assignedResources: [],
        assignedAreaIds: [],
      };

      await expect(service.create(createDto, currentUser)).rejects.toThrow(
        'Vui lòng cập nhật diện tích mặt nước cho ao trước khi bắt đầu chu kỳ nuôi',
      );
    });
  • Step 2: Run the test to verify it fails
cd backend
npx jest cycles.service.spec.ts -t "no surfaceArea set"

Expected: FAIL — currently initialStock / null computes Infinity, not a thrown
BadRequestException, so the assertion doesn’t match.

  • Step 3: Implement the guard

In backend/src/aquaculture/cycles.service.ts, change:

    const stockingDensity = createCycleDto.initialStock / pond.surfaceArea;

to:

    if (pond.surfaceArea == null) {
      throw new BadRequestException(
        'Vui lòng cập nhật diện tích mặt nước cho ao trước khi bắt đầu chu kỳ nuôi',
      );
    }

    const stockingDensity = createCycleDto.initialStock / pond.surfaceArea;
  • Step 4: Run the test to verify it passes
cd backend
npx jest cycles.service.spec.ts -t "no surfaceArea set"

Expected: PASS.

  • Step 5: Run the full cycles service test suite
cd backend
npx jest cycles.service.spec.ts

Expected: PASS (all tests, including the pre-existing ones).

  • Step 6: Commit
git add backend/src/aquaculture/cycles.service.ts backend/src/aquaculture/cycles.service.spec.ts
git commit -m "feat(aquaculture): block farming cycle creation when pond has no surfaceArea"

Task 9: Remove code from cycle→pond response mapping

Files:

  • Modify: backend/src/aquaculture/cycles.service.ts

  • Modify: backend/src/aquaculture/dto/cycle-response.dto.ts

  • Modify: backend/src/aquaculture/cycles.service.spec.ts

  • Modify: backend/src/aquaculture/cycles.controller.spec.ts

  • Step 1: Update toCycleResponse() in cycles.service.ts

Around line 526-531, change:

      pond: {
        id: pond?.id || '',
        name: pond?.name || '',
        code: pond?.code || '',
        surfaceArea: pond?.surfaceArea ? Number(pond.surfaceArea) : 0,
      },

to:

      pond: {
        id: pond?.id || '',
        name: pond?.name || '',
        surfaceArea: pond?.surfaceArea ? Number(pond.surfaceArea) : 0,
      },
  • Step 2: Update CycleResponse interface and CycleResponseDto class

In backend/src/aquaculture/dto/cycle-response.dto.ts, change the interface:

export interface CycleResponse {
  id: string;
  pondId: string;
  pond: {
    id: string;
    name: string;
    code: string;
    surfaceArea: number;
  };

to:

export interface CycleResponse {
  id: string;
  pondId: string;
  pond: {
    id: string;
    name: string;
    surfaceArea: number;
  };

And the class, change:

  @ApiProperty({
    type: 'object',
    properties: {
      id: { type: 'string' },
      name: { type: 'string' },
      code: { type: 'string' },
      surfaceArea: { type: 'number' },
    },
  })
  pond: {
    id: string;
    name: string;
    code: string;
    surfaceArea: number;
  };

to:

  @ApiProperty({
    type: 'object',
    properties: {
      id: { type: 'string' },
      name: { type: 'string' },
      surfaceArea: { type: 'number' },
    },
  })
  pond: {
    id: string;
    name: string;
    surfaceArea: number;
  };
  • Step 3: Update test fixtures

In backend/src/aquaculture/cycles.service.spec.ts, remove the code: 'TP-01', line
(around line 50) from mockPond. (Leave mockArea.code at line 35 untouched — that’s
the Area entity’s own code field, unaffected by this change.)

In backend/src/aquaculture/cycles.controller.spec.ts, remove the code: 'TP-01', line
(around line 36) from the pond object in the mock cycle response fixture.

  • Step 4: Run the tests
cd backend
npx jest cycles.service.spec.ts cycles.controller.spec.ts

Expected: PASS.

  • Step 5: Commit
git add backend/src/aquaculture/cycles.service.ts backend/src/aquaculture/dto/cycle-response.dto.ts backend/src/aquaculture/cycles.service.spec.ts backend/src/aquaculture/cycles.controller.spec.ts
git commit -m "fix(aquaculture): drop pond code from cycle response mapping"

Task 10: Remove code from daily-log→pond response mapping

Files:

  • Modify: backend/src/aquaculture/daily-logs.service.ts

  • Modify: backend/src/aquaculture/dto/daily-log-response.dto.ts

  • Modify: backend/src/aquaculture/daily-logs.service.spec.ts

  • Modify: backend/src/aquaculture/daily-logs.controller.spec.ts

  • Step 1: Update the mapping in daily-logs.service.ts

Around line 517-523, change:

      pond: log.pond
        ? {
            id: log.pond.id,
            name: log.pond.name,
            code: log.pond.code,
          }
        : undefined,

to:

      pond: log.pond
        ? {
            id: log.pond.id,
            name: log.pond.name,
          }
        : undefined,
  • Step 2: Update DailyLogResponse

In backend/src/aquaculture/dto/daily-log-response.dto.ts, change:

  pond?: {
    id: string;
    name: string;
    code: string;
  };

to:

  pond?: {
    id: string;
    name: string;
  };
  • Step 3: Update test fixtures

In backend/src/aquaculture/daily-logs.service.spec.ts, remove the code: 'KA-01',
line (around line 68) from mockPond. (Leave the nested area.code: 'KV-A' at line 80
untouched — that’s the Area entity’s own field.)

In backend/src/aquaculture/daily-logs.controller.spec.ts, change:

    pond: { id: 'pond-uuid-1', name: 'Ao 1', code: 'KA-01' },

to:

    pond: { id: 'pond-uuid-1', name: 'Ao 1' },
  • Step 4: Run the tests
cd backend
npx jest daily-logs.service.spec.ts daily-logs.controller.spec.ts

Expected: PASS.

  • Step 5: Commit
git add backend/src/aquaculture/daily-logs.service.ts backend/src/aquaculture/dto/daily-log-response.dto.ts backend/src/aquaculture/daily-logs.service.spec.ts backend/src/aquaculture/daily-logs.controller.spec.ts
git commit -m "fix(aquaculture): drop pond code from daily log response mapping"

Task 11: Remove code from device-operations→pond response mapping

Files:

  • Modify: backend/src/devices/device-operations.service.ts

  • Modify: backend/src/devices/dto/device-operations.dto.ts

  • Modify: backend/src/devices/device-operations.service.spec.ts

  • Step 1: Update the mapping in device-operations.service.ts

Around line 109-111, change:

        pond: device.pond
          ? { id: device.pond.id, name: device.pond.name, code: device.pond.code }
          : null,

to:

        pond: device.pond ? { id: device.pond.id, name: device.pond.name } : null,
  • Step 2: Update DeviceOperationsWorkspaceRowDto

In backend/src/devices/dto/device-operations.dto.ts, change:

  @ApiPropertyOptional()
  pond?: { id: string; name: string; code: string } | null;

to:

  @ApiPropertyOptional()
  pond?: { id: string; name: string } | null;

(Do not touch the unrelated code?: string | null; field a few lines above this one —
that belongs to a different class further up in the file for farm/area info, not pond.)

  • Step 3: Update test fixtures

In backend/src/devices/device-operations.service.spec.ts, there are 6 occurrences of:

            pond: { id: 'pond-1', name: 'Ao 1', code: 'A1' },

(some with 12-space indent, some with 10-space indent — match whichever indentation is
already there at each of the 6 sites). Change each to:

            pond: { id: 'pond-1', name: 'Ao 1' },
  • Step 4: Run the tests
cd backend
npx jest device-operations.service.spec.ts device-operations.controller.spec.ts

Expected: PASS.

  • Step 5: Commit
git add backend/src/devices/device-operations.service.ts backend/src/devices/dto/device-operations.dto.ts backend/src/devices/device-operations.service.spec.ts
git commit -m "fix(devices): drop pond code from device operations workspace response"

Task 12: Remove code from alert→pond response mapping

Files:

  • Modify: backend/src/devices/alert.service.ts

  • Modify: backend/src/devices/dto/alert-response.dto.ts

  • Modify: backend/src/devices/alert.service.spec.ts

  • Step 1: Update the mapping in alert.service.ts

Around line 253-259, change:

      pond: alert.pond
        ? {
            id: alert.pond.id,
            name: alert.pond.name,
            code: alert.pond.code,
          }
        : undefined,

to:

      pond: alert.pond
        ? {
            id: alert.pond.id,
            name: alert.pond.name,
          }
        : undefined,
  • Step 2: Update the response type

In backend/src/devices/dto/alert-response.dto.ts, change:

  pond?: {
    id: string;
    name: string;
    code: string;
  };

to:

  pond?: {
    id: string;
    name: string;
  };
  • Step 3: Update the test fixture

In backend/src/devices/alert.service.spec.ts, remove the code: 'POND-1', line
(around line 101) from _mockPond.

  • Step 4: Run the tests
cd backend
npx jest alert.service.spec.ts

Expected: PASS.

  • Step 5: Commit
git add backend/src/devices/alert.service.ts backend/src/devices/dto/alert-response.dto.ts backend/src/devices/alert.service.spec.ts
git commit -m "fix(devices): drop pond code from alert response mapping"

Task 13: Remove code from alert-config→pond response mapping

Files:

  • Modify: backend/src/devices/controllers/alert-config.controller.ts

  • Modify: backend/src/devices/dto/alert-config-response.dto.ts

  • Modify: backend/src/devices/alert-config.service.spec.ts

  • Step 1: Update the mapping in alert-config.controller.ts

Around line 177-183, change:

      pond: config.pond?.name
        ? {
            id: config.pond.id,
            name: config.pond.name,
            code: config.pond.code,
          }
        : undefined,

to:

      pond: config.pond?.name
        ? {
            id: config.pond.id,
            name: config.pond.name,
          }
        : undefined,
  • Step 2: Update the response type

In backend/src/devices/dto/alert-config-response.dto.ts, change:

  pond?: {
    id: string;
    name: string;
    code: string;
  };

to:

  pond?: {
    id: string;
    name: string;
  };
  • Step 3: Update the test fixture

In backend/src/devices/alert-config.service.spec.ts, remove the code: 'POND-1',
line (around line 89) from mockPond.

  • Step 4: Run the tests
cd backend
npx jest alert-config.service.spec.ts

Expected: PASS.

  • Step 5: Commit
git add backend/src/devices/controllers/alert-config.controller.ts backend/src/devices/dto/alert-config-response.dto.ts backend/src/devices/alert-config.service.spec.ts
git commit -m "fix(devices): drop pond code from alert config response mapping"

Task 14: Remove hardcoded pond codes from seed data

Files:

  • Modify: backend/src/database/seeds/demo-data.seed.ts

  • Step 1: Remove the code line from each of the 6 pond seed objects

In the ponds array (around lines 326-389), remove the code: 'POND-1A', /
code: 'POND-1B', / code: 'POND-1C', / code: 'POND-2A', / code: 'POND-2B', /
code: 'POND-2C', line from each of the 6 objects. For example, change:

      {
        id: seedId('pond:1a'),
        areaId: northArea.id,
        name: 'Ao 1A',
        code: 'POND-1A',
        surfaceArea: 5000, // 5000 m²
        depth: 1.5,
        type: 'earthen',
        status: 'active',
      },

to:

      {
        id: seedId('pond:1a'),
        areaId: northArea.id,
        name: 'Ao 1A',
        surfaceArea: 5000, // 5000 m²
        depth: 1.5,
        type: 'earthen',
        status: 'active',
      },

…and the same pattern for the other 5 pond objects. Leave the area codes
(NORTH-AREA, SOUTH-AREA) and organization code (BENTRE-SHRIMP-01) untouched.

  • Step 2: Verify the seed still runs
cd backend
bun run seed:demo

Expected: completes without error, logs 💧 Creating ponds... and continues past it.

  • Step 3: Commit
git add backend/src/database/seeds/demo-data.seed.ts
git commit -m "fix(database): remove hardcoded pond codes from demo seed data"

Task 15: Full backend verification

  • Step 1: Run the full backend test suite
cd backend
bun run test

Expected: all tests PASS.

  • Step 2: Run Biome
bun run check

Expected: no errors (fix anything Biome flags before moving on).

  • Step 3: Build
cd backend
bun run build

Expected: compiles with no TypeScript errors.


Frontend

Task 16: Update farm.schema.ts and farm.types.ts

Files:

  • Modify: frontend/src/schemas/farm.schema.ts

  • Modify: frontend/src/types/farm.types.ts

  • Step 1: Update pondSchema, createPondSchema, updatePondSchema

In frontend/src/schemas/farm.schema.ts, change:

// Pond entity schema (from API response)
export const pondSchema = z.object({
  id: uuidSchema,
  areaId: uuidSchema,
  organizationId: uuidSchema,
  name: z.string(),
  code: z.string().optional(),
  surfaceArea: z.number().positive(),
  depth: z.number().positive().optional(),
  type: pondTypeSchema,
  status: pondStatusSchema,
  currentCycleId: uuidSchema.optional(),
  assignedDevices: z.array(z.string()).optional(),
  assignedUsers: z.array(z.string()).optional(),
  createdAt: z.string(),
  updatedAt: z.string(),
});

// Create Pond DTO schema
export const createPondSchema = z.object({
  areaId: uuidSchema,
  name: z
    .string()
    .min(2, 'Tên phải có ít nhất 2 ký tự')
    .max(100, 'Tên không được vượt quá 100 ký tự'),
  code: z
    .string()
    .max(50, 'Mã không được vượt quá 50 ký tự')
    .regex(/^[A-Za-z0-9-_]*$/, 'Mã chỉ được chứa chữ cái, số, dấu gạch ngang và gạch dưới')
    .optional(),
  surfaceArea: z.number().positive('Diện tích mặt nước phải lớn hơn 0'),
  depth: z.number().positive('Độ sâu phải là số dương'),
  type: pondTypeSchema,
  status: pondStatusSchema.optional(),
});

// Update Pond DTO schema
export const updatePondSchema = z.object({
  name: z
    .string()
    .min(2, 'Tên phải có ít nhất 2 ký tự')
    .max(100, 'Tên không được vượt quá 100 ký tự')
    .optional(),
  code: z
    .string()
    .max(50, 'Mã không được vượt quá 50 ký tự')
    .regex(/^[A-Za-z0-9-_]*$/, 'Mã chỉ được chứa chữ cái, số, dấu gạch ngang và gạch dưới')
    .optional(),
  surfaceArea: z.number().positive('Diện tích mặt nước phải lớn hơn 0').optional(),
  depth: z.number().positive('Độ sâu phải là số dương').optional(),
  type: pondTypeSchema.optional(),
  status: pondStatusSchema.optional(),
});

to:

// Pond entity schema (from API response)
export const pondSchema = z.object({
  id: uuidSchema,
  areaId: uuidSchema,
  organizationId: uuidSchema,
  name: z.string(),
  surfaceArea: z.number().positive().optional(),
  depth: z.number().positive().optional(),
  type: pondTypeSchema.optional(),
  status: pondStatusSchema,
  currentCycleId: uuidSchema.optional(),
  assignedDevices: z.array(z.string()).optional(),
  assignedUsers: z.array(z.string()).optional(),
  createdAt: z.string(),
  updatedAt: z.string(),
});

// Create Pond DTO schema
export const createPondSchema = z.object({
  areaId: uuidSchema,
  name: z
    .string()
    .min(2, 'Tên phải có ít nhất 2 ký tự')
    .max(100, 'Tên không được vượt quá 100 ký tự'),
  surfaceArea: z.number().positive('Diện tích mặt nước phải lớn hơn 0').optional(),
  depth: z.number().positive('Độ sâu phải là số dương').optional(),
  type: pondTypeSchema.optional(),
  status: pondStatusSchema.optional(),
});

// Update Pond DTO schema
export const updatePondSchema = z.object({
  name: z
    .string()
    .min(2, 'Tên phải có ít nhất 2 ký tự')
    .max(100, 'Tên không được vượt quá 100 ký tự')
    .optional(),
  surfaceArea: z.number().positive('Diện tích mặt nước phải lớn hơn 0').optional(),
  depth: z.number().positive('Độ sâu phải là số dương').optional(),
  type: pondTypeSchema.optional(),
  status: pondStatusSchema.optional(),
});
  • Step 2: Update the Pond, CreatePondDto, UpdatePondDto interfaces

In frontend/src/types/farm.types.ts, change:

export interface Pond {
  id: string;
  areaId: string;
  areaName?: string;
  organizationId: string;
  name: string;
  code?: string;
  surfaceArea: number;
  depth?: number;
  type: 'lined' | 'earthen';
  status: 'active' | 'inactive' | 'maintenance';
  currentCycleId?: string;
  assignedDevices?: string[];
  assignedUsers?: string[];
  createdAt: string;
  updatedAt: string;
}

to:

export interface Pond {
  id: string;
  areaId: string;
  areaName?: string;
  organizationId: string;
  name: string;
  surfaceArea?: number;
  depth?: number;
  type?: 'lined' | 'earthen';
  status: 'active' | 'inactive' | 'maintenance';
  currentCycleId?: string;
  assignedDevices?: string[];
  assignedUsers?: string[];
  createdAt: string;
  updatedAt: string;
}

And change:

export interface CreatePondDto {
  areaId: string;
  name: string;
  code?: string;
  surfaceArea: number;
  depth: number;
  type: 'lined' | 'earthen';
  status?: 'active' | 'inactive' | 'maintenance';
}

export interface UpdatePondDto {
  name?: string;
  code?: string;
  surfaceArea?: number;
  depth?: number;
  type?: 'lined' | 'earthen';
  status?: 'active' | 'inactive' | 'maintenance';
}

to:

export interface CreatePondDto {
  areaId: string;
  name: string;
  surfaceArea?: number;
  depth?: number;
  type?: 'lined' | 'earthen';
  status?: 'active' | 'inactive' | 'maintenance';
}

export interface UpdatePondDto {
  name?: string;
  surfaceArea?: number;
  depth?: number;
  type?: 'lined' | 'earthen';
  status?: 'active' | 'inactive' | 'maintenance';
}
  • Step 3: Commit
git add frontend/src/schemas/farm.schema.ts frontend/src/types/farm.types.ts
git commit -m "feat(frontend): drop pond code and make surfaceArea/depth/type optional in farm types"

(This commit will leave several frontend files failing to typecheck — that’s expected
and fixed by the remaining tasks. Don’t run bun run build until Task 25.)


Task 17: Update cycle.schema.ts (pondRefSchema) and cycle.types.ts (PondRef)

Files:

  • Modify: frontend/src/schemas/cycle.schema.ts

  • Modify: frontend/src/types/cycle.types.ts

  • Step 1: Update pondRefSchema

In frontend/src/schemas/cycle.schema.ts, change:

// Pond reference schema (lightweight)
export const pondRefSchema = z.object({
  id: uuidSchema,
  name: z.string(),
  code: z.string(),
  surfaceArea: z.number().positive(),
});

to:

// Pond reference schema (lightweight)
export const pondRefSchema = z.object({
  id: uuidSchema,
  name: z.string(),
  surfaceArea: z.number().positive().optional(),
});
  • Step 2: Update PondRef

In frontend/src/types/cycle.types.ts, change:

// Pond (lightweight reference)
export interface PondRef {
  id: string;
  name: string;
  code: string;
  surfaceArea: number; // m²
}

to:

// Pond (lightweight reference)
export interface PondRef {
  id: string;
  name: string;
  surfaceArea?: number; // m²
}
  • Step 3: Commit
git add frontend/src/schemas/cycle.schema.ts frontend/src/types/cycle.types.ts
git commit -m "feat(frontend): drop pond code from cycle's pond reference type"

Task 18: Update device.types.ts and device-operations.types.ts

Files:

  • Modify: frontend/src/types/device.types.ts

  • Modify: frontend/src/types/device-operations.types.ts

  • Step 1: Update Device.pond

In frontend/src/types/device.types.ts, change:

  pond?: {
    id: string;
    name: string;
    code: string;
  };

to:

  pond?: {
    id: string;
    name: string;
  };
  • Step 2: Update DeviceOperationsWorkspaceRow.pond

In frontend/src/types/device-operations.types.ts, change:

  pond?: { id: string; name: string; code: string } | null;

to:

  pond?: { id: string; name: string } | null;

(Do not touch DeviceOperationsFarm.code a few lines above — that’s an unrelated
farm/area code field.)

  • Step 3: Commit
git add frontend/src/types/device.types.ts frontend/src/types/device-operations.types.ts
git commit -m "feat(frontend): drop pond code from device pond reference types"

Task 19: Restructure PondForm.tsx — remove code field, add progressive disclosure

Files:

  • Modify: frontend/src/components/farm/PondForm.tsx

  • Create: frontend/src/components/farm/PondForm.spec.tsx

  • Step 1: Write the failing test

Create frontend/src/components/farm/PondForm.spec.tsx:

import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import { PondForm } from '@/components/farm/PondForm';
import { renderWithProviders } from '@/test/renderWithProviders';

const mockCreateMutation = vi.fn().mockResolvedValue({
  id: 'pond-new',
  areaId: 'area-1',
  name: 'Ao mới',
  status: 'active',
});

vi.mock('@/hooks/useFarms', () => ({
  useAreas: () => ({
    data: { data: [{ id: 'area-1', name: 'Khu A' }] },
    isLoading: false,
  }),
  useCreatePond: () => ({ mutateAsync: mockCreateMutation, isPending: false }),
  useUpdatePond: () => ({ mutateAsync: vi.fn(), isPending: false }),
  usePondDevices: () => ({ data: [] }),
  usePond: () => ({ data: undefined }),
  useOrgUsers: () => ({ data: { data: [] }, isLoading: false }),
  useAssignDevice: () => ({ mutateAsync: vi.fn(), isPending: false }),
  useUnassignDevice: () => ({ mutateAsync: vi.fn(), isPending: false }),
  useAssignUser: () => ({ mutateAsync: vi.fn(), isPending: false }),
  useUnassignUser: () => ({ mutateAsync: vi.fn(), isPending: false }),
}));

vi.mock('@/services/device.service', () => ({
  deviceService: { getAllDevices: vi.fn().mockResolvedValue({ data: [] }) },
}));

describe('PondForm', () => {
  it('does not render a pond code field', () => {
    renderWithProviders(<PondForm visible onCancel={vi.fn()} defaultAreaId="area-1" />);

    expect(screen.queryByLabelText('Mã ao')).not.toBeInTheDocument();
  });

  it('keeps advanced options collapsed by default and submits with only area + name', async () => {
    renderWithProviders(<PondForm visible onCancel={vi.fn()} defaultAreaId="area-1" />);

    expect(screen.queryByLabelText('Diện tích mặt nước')).not.toBeInTheDocument();

    await userEvent.type(screen.getByLabelText('Tên ao'), 'Ao mới');
    await userEvent.click(screen.getByRole('button', { name: 'Tạo ao & tiếp tục' }));

    await waitFor(() => {
      expect(mockCreateMutation).toHaveBeenCalledWith(
        expect.objectContaining({ areaId: 'area-1', name: 'Ao mới' }),
      );
    });
  });

  it('expands advanced options by default when editing a pond that already has them set', () => {
    renderWithProviders(
      <PondForm
        visible
        onCancel={vi.fn()}
        pond={{
          id: 'pond-1',
          areaId: 'area-1',
          organizationId: 'org-1',
          name: 'Ao 1',
          surfaceArea: 1000,
          depth: 1.5,
          type: 'lined',
          status: 'active',
          createdAt: '',
          updatedAt: '',
        }}
      />,
    );

    expect(screen.getByLabelText('Diện tích mặt nước')).toBeInTheDocument();
  });
});
  • Step 2: Run the tests to verify they fail
cd frontend
bun run test -- PondForm.spec.tsx

Expected: FAIL — PondForm.tsx still renders the “Mã ao” field and doesn’t collapse
the other fields, so the first two assertions fail; the third passes trivially since
all fields are currently always visible (that’s fine, it’ll stay green after the
refactor for a more meaningful reason).

  • Step 3: Restructure PondForm.tsx

Add Collapse to the antd import list. Change:

import {
  Avatar,
  Button,
  Card,
  Col,
  Divider,
  Form,
  Grid,
  Input,
  InputNumber,
  Modal,
  Row,
  Select,
  Space,
  Steps,
  Tag,
  Typography,
} from 'antd';

to:

import {
  Avatar,
  Button,
  Card,
  Col,
  Collapse,
  Divider,
  Form,
  Grid,
  Input,
  InputNumber,
  Modal,
  Row,
  Select,
  Space,
  Steps,
  Tag,
  Typography,
} from 'antd';

Replace the whole BasicInfoStep function body with:

function BasicInfoStep({
  form,
  isEditMode,
  areasLoading,
  areaOptions,
  typeOptions,
  statusOptions,
}: {
  form: ReturnType<typeof Form.useForm<CreatePondInput>>[0];
  isEditMode: boolean;
  areasLoading: boolean;
  areaOptions: { label: string; value: string }[];
  typeOptions: { label: string; value: string }[];
  statusOptions: { label: string; value: string }[];
}) {
  return (
    <Form
      form={form}
      layout="vertical"
      requiredMark={false}
      autoComplete="off"
      initialValues={{ type: 'lined', status: 'active' }}
    >
      <Form.Item label="Khu vực" name="areaId" rules={pondFormRules.areaId}>
        <Select
          placeholder="Chọn khu vực"
          loading={areasLoading}
          options={areaOptions}
          size="large"
          disabled={isEditMode}
          showSearch
          filterOption={(input, option) =>
            ((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
          }
        />
      </Form.Item>

      <Form.Item label="Tên ao" name="name" rules={pondFormRules.name}>
        <Input placeholder="VD: Ao A1" size="large" allowClear showCount maxLength={100} />
      </Form.Item>

      <Collapse
        ghost
        defaultActiveKey={isEditMode ? ['advanced'] : []}
        items={[
          {
            key: 'advanced',
            label: 'Tùy chọn nâng cao',
            children: (
              <>
                <Row gutter={16}>
                  <Col span={12}>
                    <Form.Item
                      label="Diện tích mặt nước"
                      name="surfaceArea"
                      rules={pondFormRules.surfaceArea}
                    >
                      <InputNumber
                        placeholder="VD: 1000"
                        size="large"
                        min={0.01}
                        precision={2}
                        controls={false}
                        addonAfter="m²"
                        style={{ width: '100%' }}
                      />
                    </Form.Item>
                  </Col>
                  <Col span={12}>
                    <Form.Item label="Độ sâu" name="depth" rules={pondFormRules.depth}>
                      <InputNumber
                        placeholder="VD: 1.5"
                        size="large"
                        min={0}
                        precision={2}
                        controls={false}
                        addonAfter="m"
                        style={{ width: '100%' }}
                      />
                    </Form.Item>
                  </Col>
                </Row>

                <Row gutter={16}>
                  <Col span={12}>
                    <Form.Item label="Loại ao" name="type" rules={pondFormRules.type}>
                      <Select placeholder="Chọn loại ao" size="large" options={typeOptions} />
                    </Form.Item>
                  </Col>
                  <Col span={12}>
                    <Form.Item label="Trạng thái" name="status" rules={pondFormRules.status}>
                      <Select placeholder="Chọn trạng thái" size="large" options={statusOptions} />
                    </Form.Item>
                  </Col>
                </Row>
              </>
            ),
          },
        ]}
      />
    </Form>
  );
}

In the main PondForm component, change the form.setFieldsValue call (inside the
useEffect):

      if (pond) {
        form.setFieldsValue({
          areaId: pond.areaId,
          name: pond.name,
          code: pond.code,
          surfaceArea: pond.surfaceArea,
          depth: pond.depth,
          type: pond.type,
          status: pond.status,
        });
      } else {

to:

      if (pond) {
        form.setFieldsValue({
          areaId: pond.areaId,
          name: pond.name,
          surfaceArea: pond.surfaceArea,
          depth: pond.depth,
          type: pond.type,
          status: pond.status,
        });
      } else {

And in handleStep1Submit, change:

      if (isEditMode && pond) {
        const updateDto: UpdatePondDto = {
          name: values.name,
          code: values.code,
          surfaceArea: values.surfaceArea,
          depth: values.depth,
          type: values.type,
          status: values.status,
        };
        await updateMutation.mutateAsync({ id: pond.id, dto: updateDto });
        // Chuyển sang step 2 để quản lý thiết bị & nhân viên
        setCurrentStep(1);
      } else {
        const createDto: CreatePondDto = {
          areaId: values.areaId,
          name: values.name,
          code: values.code,
          surfaceArea: values.surfaceArea,
          depth: values.depth,
          type: values.type,
          status: values.status,
        };

to:

      if (isEditMode && pond) {
        const updateDto: UpdatePondDto = {
          name: values.name,
          surfaceArea: values.surfaceArea,
          depth: values.depth,
          type: values.type,
          status: values.status,
        };
        await updateMutation.mutateAsync({ id: pond.id, dto: updateDto });
        // Chuyển sang step 2 để quản lý thiết bị & nhân viên
        setCurrentStep(1);
      } else {
        const createDto: CreatePondDto = {
          areaId: values.areaId,
          name: values.name,
          surfaceArea: values.surfaceArea,
          depth: values.depth,
          type: values.type,
          status: values.status,
        };
  • Step 4: Run the tests to verify they pass
cd frontend
bun run test -- PondForm.spec.tsx

Expected: PASS, all 3 tests green.

  • Step 5: Commit
git add frontend/src/components/farm/PondForm.tsx frontend/src/components/farm/PondForm.spec.tsx
git commit -m "feat(farm): remove pond code field, add progressive disclosure to PondForm"

Task 20: PondCard.tsx — remove code display, guard surfaceArea

Files:

  • Modify: frontend/src/components/farm/PondCard.tsx

  • Step 1: Remove the code display block

Change:

            {pond.name}
          </div>
          {pond.code && (
            <div
              style={{
                fontFamily: "'JetBrains Mono', monospace",
                fontSize: 11,
                color: '#9aa0a9',
                marginTop: 2,
              }}
            >
              {pond.code}
            </div>
          )}
        </div>

to:

            {pond.name}
          </div>
        </div>
  • Step 2: Guard the surfaceArea display

Change:

          <Text strong style={{ fontSize: 13 }}>
            {pond.surfaceArea.toLocaleString('vi-VN')} m²
          </Text>

to:

          <Text strong style={{ fontSize: 13 }}>
            {pond.surfaceArea != null ? `${pond.surfaceArea.toLocaleString('vi-VN')} m²` : '—'}
          </Text>
  • Step 3: Verify with the dev server or a quick typecheck
cd frontend
bun run build

Expected: no new TypeScript errors introduced by this file (other files may still fail
until their tasks are done — check the error output only mentions files not yet fixed).

  • Step 4: Commit
git add frontend/src/components/farm/PondCard.tsx
git commit -m "fix(farm): remove pond code display and guard null surfaceArea in PondCard"

Task 21: PondTable.tsx — remove “Mã ao” column, guard surfaceArea render

Files:

  • Modify: frontend/src/components/farm/PondTable.tsx

  • Step 1: Remove the “Mã ao” column

Delete this whole column definition:

    {
      title: 'Mã ao',
      dataIndex: 'code',
      key: 'code',
      sorter: true,
      showSorterTooltip: { title: 'Sắp xếp theo mã' },
      render: (code: string) => (
        <Text type="secondary" style={{ fontFamily: 'monospace' }}>
          {code || '-'}
        </Text>
      ),
    },
  • Step 2: Guard the surfaceArea column render

Change:

    {
      title: 'Diện tích',
      dataIndex: 'surfaceArea',
      key: 'surfaceArea',
      sorter: true,
      showSorterTooltip: { title: 'Sắp xếp theo diện tích' },
      render: (surfaceArea: number) => <Text strong>{surfaceArea.toLocaleString()} m²</Text>,
      align: 'right' as const,
    },

to:

    {
      title: 'Diện tích',
      dataIndex: 'surfaceArea',
      key: 'surfaceArea',
      sorter: true,
      showSorterTooltip: { title: 'Sắp xếp theo diện tích' },
      render: (surfaceArea: number | null | undefined) => (
        <Text strong>{surfaceArea != null ? `${surfaceArea.toLocaleString()} m²` : '—'}</Text>
      ),
      align: 'right' as const,
    },
  • Step 3: Commit
git add frontend/src/components/farm/PondTable.tsx
git commit -m "fix(farm): remove Mã ao column and guard null surfaceArea in PondTable"

Task 22: PondDetail.tsx — remove code display, guard surfaceArea and type

Files:

  • Modify: frontend/src/pages/farm/PondDetail.tsx

  • Step 1: Remove the code subtitle

Change:

      <PageHeader
        title={pond.name}
        subtitle={pond.code}
        onBack={handleBack}

to:

      <PageHeader
        title={pond.name}
        onBack={handleBack}
  • Step 2: Remove the “Mã ao” Descriptions.Item, guard type and surfaceArea

Change:

                  <Descriptions column={2} size="middle">
                    <Descriptions.Item label="Tên ao">
                      <span style={{ fontWeight: 500 }}>{pond.name}</span>
                    </Descriptions.Item>
                    <Descriptions.Item label="Mã ao">
                      {pond.code ? (
                        <Tag color="blue">{pond.code}</Tag>
                      ) : (
                        <Text type="secondary"></Text>
                      )}
                    </Descriptions.Item>
                    <Descriptions.Item label="Loại ao">
                      <Tag color={getTypeColor(pond.type)}>{getTypeText(pond.type)}</Tag>
                    </Descriptions.Item>
                    <Descriptions.Item label="Trạng thái">
                      <Tag color={getStatusColor(pond.status)}>{getStatusText(pond.status)}</Tag>
                    </Descriptions.Item>
                    <Descriptions.Item label="Diện tích mặt nước">
                      <span style={{ fontWeight: 600, color: '#1890ff' }}>
                        {pond.surfaceArea.toLocaleString('vi-VN')} m²
                      </span>
                    </Descriptions.Item>

to:

                  <Descriptions column={2} size="middle">
                    <Descriptions.Item label="Tên ao">
                      <span style={{ fontWeight: 500 }}>{pond.name}</span>
                    </Descriptions.Item>
                    <Descriptions.Item label="Loại ao">
                      {pond.type ? (
                        <Tag color={getTypeColor(pond.type)}>{getTypeText(pond.type)}</Tag>
                      ) : (
                        <Text type="secondary"></Text>
                      )}
                    </Descriptions.Item>
                    <Descriptions.Item label="Trạng thái">
                      <Tag color={getStatusColor(pond.status)}>{getStatusText(pond.status)}</Tag>
                    </Descriptions.Item>
                    <Descriptions.Item label="Diện tích mặt nước">
                      <span style={{ fontWeight: 600, color: '#1890ff' }}>
                        {pond.surfaceArea != null
                          ? `${pond.surfaceArea.toLocaleString('vi-VN')} m²`
                          : '—'}
                      </span>
                    </Descriptions.Item>
  • Step 3: Commit
git add frontend/src/pages/farm/PondDetail.tsx
git commit -m "fix(farm): remove Mã ao display and guard null type/surfaceArea in PondDetail"

Task 23: Device pages — remove pond code display

Files:

  • Modify: frontend/src/pages/devices/DeviceList.tsx

  • Modify: frontend/src/pages/devices/DeviceDashboard.tsx

  • Modify: frontend/src/pages/devices/DeviceDetail.tsx

  • Modify: frontend/src/pages/devices/DeviceOperationsPage.tsx

  • Step 1: DeviceList.tsx — two occurrences. Change:

          <Tag color="blue">
            {pond.name} ({pond.code})
          </Tag>

to:

          <Tag color="blue">{pond.name}</Tag>

And change:

                  <Tag color="blue">
                    {device.pond.name} ({device.pond.code})
                  </Tag>

to:

                  <Tag color="blue">{device.pond.name}</Tag>
  • Step 2: DeviceDashboard.tsx — change:
          <span>
            {pond.name} ({pond.code})
          </span>

to:

          <span>{pond.name}</span>
  • Step 3: DeviceDetail.tsx — change:
                  <Tag color="blue">
                    {device.pond.name} ({device.pond.code})
                  </Tag>

to:

                  <Tag color="blue">{device.pond.name}</Tag>
  • Step 4: DeviceOperationsPage.tsx — change:
      map.set(row.pond.id, {
        value: row.pond.id,
        label: `${row.pond.name} (${row.pond.code})`,
      });

to:

      map.set(row.pond.id, {
        value: row.pond.id,
        label: row.pond.name,
      });

And change:

      render: (_, row) => (row.pond ? `${row.pond.name} (${row.pond.code})` : 'Chưa gán'),

to:

      render: (_, row) => (row.pond ? row.pond.name : 'Chưa gán'),
  • Step 5: Commit
git add frontend/src/pages/devices/DeviceList.tsx frontend/src/pages/devices/DeviceDashboard.tsx frontend/src/pages/devices/DeviceDetail.tsx frontend/src/pages/devices/DeviceOperationsPage.tsx
git commit -m "fix(devices): remove pond code display across device pages"

Task 24: Cycle pages — remove pond code display

Files:

  • Modify: frontend/src/pages/cycles/CycleDetail.tsx

  • Modify: frontend/src/pages/cycles/CycleEdit.tsx

  • Modify: frontend/src/pages/cycles/CycleList.tsx

  • Modify: frontend/src/pages/cycles/CyclesDashboard.tsx

  • Step 1: CycleDetail.tsx — change:

                <Tag color="blue">
                  {selectedCycle.pond?.name} ({selectedCycle.pond?.code ?? ''})
                </Tag>

to:

                <Tag color="blue">{selectedCycle.pond?.name}</Tag>
  • Step 2: CycleEdit.tsx — change:
                <div className="font-medium">
                  {selectedCycle.pond?.name}
                  {selectedCycle.pond?.code ? ` (${selectedCycle.pond.code})` : ''}
                </div>

to:

                <div className="font-medium">{selectedCycle.pond?.name}</div>
  • Step 3: CycleList.tsx — two occurrences. Change:
      render: (pond: { name?: string; code?: string } | undefined) => (
        <Tag color="blue">
          {pond?.name} ({pond?.code})
        </Tag>
      ),

to:

      render: (pond: { name?: string } | undefined) => <Tag color="blue">{pond?.name}</Tag>,

And change:

                  <Tag color="blue">
                    {cycle.pond?.name} ({cycle.pond?.code})
                  </Tag>

to:

                  <Tag color="blue">{cycle.pond?.name}</Tag>
  • Step 4: CyclesDashboard.tsx — change:
      render: (pond: { name: string; code: string } | undefined) => (
        <span>
          {pond?.name} ({pond?.code})
        </span>
      ),

to:

      render: (pond: { name: string } | undefined) => <span>{pond?.name}</span>,
  • Step 5: Commit
git add frontend/src/pages/cycles/CycleDetail.tsx frontend/src/pages/cycles/CycleEdit.tsx frontend/src/pages/cycles/CycleList.tsx frontend/src/pages/cycles/CyclesDashboard.tsx
git commit -m "fix(cycles): remove pond code display across cycle pages"

Task 25: CycleWizard.tsx — remove code reference, guard surfaceArea

Files:

  • Modify: frontend/src/pages/cycles/CycleWizard.tsx

  • Step 1: Remove code from the pond options label, guard surfaceArea

Change:

  const pondOptions = ponds.map((p) => ({
    label: `${p.name}${p.code ? ` (${p.code})` : ''} - ${p.surfaceArea.toLocaleString('vi-VN')} m²`,
    value: p.id,
  }));

to:

  const pondOptions = ponds.map((p) => ({
    label:
      p.surfaceArea != null
        ? `${p.name} - ${p.surfaceArea.toLocaleString('vi-VN')} m²`
        : p.name,
    value: p.id,
  }));
  • Step 2: Guard surfaceArea in the stocking density alert description

Change:

            {calculatedDensity !== null && (
              <Alert
                message="Mật độ thả giống"
                description={`Mật độ thả: ${calculatedDensity.toFixed(2)} con/m² (${form.getFieldValue('initialStock')?.toLocaleString('vi-VN')} con / ${ponds.find((p) => p.id === form.getFieldValue('pondId'))?.surfaceArea.toLocaleString('vi-VN')} m²)`}
                type="success"
                showIcon
                style={{ marginBottom: 16 }}
              />
            )}

to:

            {calculatedDensity !== null && (
              <Alert
                message="Mật độ thả giống"
                description={`Mật độ thả: ${calculatedDensity.toFixed(2)} con/m² (${form.getFieldValue('initialStock')?.toLocaleString('vi-VN')} con / ${ponds
                  .find((p) => p.id === form.getFieldValue('pondId'))
                  ?.surfaceArea?.toLocaleString('vi-VN')} m²)`}
                type="success"
                showIcon
                style={{ marginBottom: 16 }}
              />
            )}

(The pond && pond.surfaceArea > 0 guard in the useEffect that computes
calculatedDensity already handles null/undefined safely — no change needed there.)

  • Step 3: Commit
git add frontend/src/pages/cycles/CycleWizard.tsx
git commit -m "fix(cycles): remove pond code reference and guard null surfaceArea in CycleWizard"

Task 26: Full frontend verification

  • Step 1: Run the full frontend test suite
cd frontend
bun run test

Expected: all tests PASS, including the new PondForm.spec.tsx.

  • Step 2: Run Biome
bun run check

Expected: no errors.

  • Step 3: Build
cd frontend
bun run build

Expected: compiles cleanly — this is the point where any missed call site (a
pond.code reference not caught by this plan) would surface as a TypeScript error. If
one appears, grep for the exact property access and remove it following the same
pattern as the task that touched the neighboring code.


Task 27: Manual smoke test

  • Step 1: Start both dev servers
bun run backend:dev
bun run frontend:dev
  • Step 2: Walk through the golden path in the browser
  1. Go to a farm’s pond list, click “Tạo ao mới”.
  2. Confirm there is no “Mã ao” field anywhere in the form.
  3. Confirm “Diện tích mặt nước”, “Độ sâu”, “Loại ao”, “Trạng thái” are hidden behind a
    collapsed “Tùy chọn nâng cao” section.
  4. Fill in only “Khu vực” and “Tên ao”, submit. Confirm the pond is created without a
    400 error and without a validation nag about surface area/depth/type.
  5. Open the newly created pond’s detail page — confirm it renders without a JS error
    (no “Mã ao” row, “Diện tích mặt nước” shows “—”, “Loại ao” shows “—”).
  6. Go to the pond list table/cards view — confirm no crash, no “Mã ao” column, surface
    area shows “—” for the new pond.
  7. Try creating a farming cycle on that pond — confirm you get a clear error telling you
    to set the surface area first, instead of a silent NaN or a server 500.
  8. Edit the pond, open “Tùy chọn nâng cao”, fill in surface area/depth/type, save.
    Create a cycle on it again — confirm stocking density now calculates correctly.
  9. Spot-check a device assigned to a pond (Device List/Detail/Dashboard/Operations
    pages) — confirm the pond is shown as just its name, no crash, no stray (undefined).
  • Step 3: Report results

Note any unexpected behavior found during the smoke test as a follow-up, rather than
silently patching around it — if something surfaces here that isn’t covered by the
tasks above, stop and reassess before considering the plan complete.