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.tsStep 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.tsStep 1: Remove the
codecolumn and relaxsurfaceArea/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.tsTest:
backend/src/farm/ponds.service.spec.tsStep 1: Write the failing test — add a new test inside the existing
describe('create', ...)block inbackend/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.tsModify:
backend/src/farm/ponds.service.spec.tsModify:
backend/src/farm/controllers/ponds.controller.spec.tsStep 1: Remove the unused
Notimport
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
codefield increate()
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
codefield inupdate()
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.tsfixtures 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.tsfixtures
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.tsModify:
backend/src/farm/dto/pond-response.dto.tsStep 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()inponds.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.tsStep 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.tsModify:
backend/src/devices/dto/device-response.dto.tsModify:
backend/src/devices/interfaces/device-response.interface.tsModify:
backend/src/devices/devices.service.spec.tsStep 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
DeviceResponseinterface
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.tsTest:
backend/src/aquaculture/cycles.service.spec.tsStep 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.tsModify:
backend/src/aquaculture/dto/cycle-response.dto.tsModify:
backend/src/aquaculture/cycles.service.spec.tsModify:
backend/src/aquaculture/cycles.controller.spec.tsStep 1: Update
toCycleResponse()incycles.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
CycleResponseinterface andCycleResponseDtoclass
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.tsModify:
backend/src/aquaculture/dto/daily-log-response.dto.tsModify:
backend/src/aquaculture/daily-logs.service.spec.tsModify:
backend/src/aquaculture/daily-logs.controller.spec.tsStep 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.tsModify:
backend/src/devices/dto/device-operations.dto.tsModify:
backend/src/devices/device-operations.service.spec.tsStep 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.tsModify:
backend/src/devices/dto/alert-response.dto.tsModify:
backend/src/devices/alert.service.spec.tsStep 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.tsModify:
backend/src/devices/dto/alert-config-response.dto.tsModify:
backend/src/devices/alert-config.service.spec.tsStep 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.tsStep 1: Remove the
codeline 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.tsModify:
frontend/src/types/farm.types.tsStep 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,UpdatePondDtointerfaces
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.tsModify:
frontend/src/types/cycle.types.tsStep 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.tsModify:
frontend/src/types/device-operations.types.tsStep 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.tsxCreate:
frontend/src/components/farm/PondForm.spec.tsxStep 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.tsxStep 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.tsxStep 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.tsxStep 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.tsxModify:
frontend/src/pages/devices/DeviceDashboard.tsxModify:
frontend/src/pages/devices/DeviceDetail.tsxModify:
frontend/src/pages/devices/DeviceOperationsPage.tsxStep 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.tsxModify:
frontend/src/pages/cycles/CycleEdit.tsxModify:
frontend/src/pages/cycles/CycleList.tsxModify:
frontend/src/pages/cycles/CyclesDashboard.tsxStep 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.tsxStep 1: Remove
codefrom the pond options label, guardsurfaceArea
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
surfaceAreain 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
- Go to a farm’s pond list, click “Tạo ao mới”.
- Confirm there is no “Mã ao” field anywhere in the form.
- 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. - 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. - 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 “—”). - Go to the pond list table/cards view — confirm no crash, no “Mã ao” column, surface
area shows “—” for the new pond. - 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. - 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. - 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.