Admin Dashboard Phase 2.2 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: Implement Phase 2.2 Admin dashboard UI as an architecture-first, design-system-based page that consumes the /dashboard/admin aggregate contract and satisfies section/empty-state acceptance criteria.

Architecture: The Admin dashboard page becomes a composition root that only orchestrates loading/error/empty flows, page header, KPI cards, and eight focused Admin section components. Each section lives in its own file with typed props and design-system primitives (PageHeader, StatsCard, DataTable, EmptyState, LoadingSkeleton) to keep boundaries clear and maintainable. Tests are written first at page level to validate section presence, empty state, and absence of Super Admin-only content.

Tech Stack: React 19 + TypeScript, Ant Design 6, TanStack Query, Vitest + Testing Library, Bun, Biome.


File Structure Map

Modify

  • frontend/src/pages/dashboard/admin/AdminDashboard.tsx
    • Replace placeholder-only rendering with architecture-first page composition.
  • frontend/src/types/dashboard/admin-dashboard.types.ts
    • Replace placeholder shape with full typed Admin contract matching backend DTO.

Create

  • frontend/src/pages/dashboard/admin/AdminDashboard.spec.tsx
    • Component tests for section rendering, empty state, and role-sensitive absence.
  • frontend/src/pages/dashboard/admin/components/OrganizationOverview.tsx
    • Admin organization summary block.
  • frontend/src/pages/dashboard/admin/components/OrganizationDeviceSummary.tsx
    • Device overview block.
  • frontend/src/pages/dashboard/admin/components/ReportsSnapshot.tsx
    • Reports snapshot block.
  • frontend/src/pages/dashboard/admin/components/FarmPerformanceTable.tsx
    • Performance table with visible-limit behavior.
  • frontend/src/pages/dashboard/admin/components/FarmRanking.tsx
    • Ranking table.
  • frontend/src/pages/dashboard/admin/components/AdminAlertCenter.tsx
    • Alert center table.
  • frontend/src/pages/dashboard/admin/components/DataInterventionQueue.tsx
    • Intervention queue table.
  • frontend/src/pages/dashboard/admin/components/OwnerStaffActivity.tsx
    • Owner/staff activity table.

Verification Commands

  • bun --cwd frontend run test -- src/pages/dashboard/admin/AdminDashboard.spec.tsx
  • bun --cwd frontend run test -- src/pages/dashboard
  • bun run check

Task 1: Write failing Admin dashboard tests (RED)

Files:

  • Create: frontend/src/pages/dashboard/admin/AdminDashboard.spec.tsx

  • Test: frontend/src/pages/dashboard/admin/AdminDashboard.spec.tsx

  • Step 1: Write the failing test file with required Phase 2.2 expectations

import { screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { AdminDashboard } from '@/pages/dashboard/admin/AdminDashboard';
import { renderWithProviders } from '@/test/renderWithProviders';
import type { AdminDashboardResponse } from '@/types/dashboard/admin-dashboard.types';

const useAdminDashboardMock = vi.fn();

vi.mock('@/hooks/dashboard/useAdminDashboard', () => ({
  useAdminDashboard: () => useAdminDashboardMock(),
}));

const buildResponse = (): AdminDashboardResponse => ({
  generatedAt: '2026-04-25T00:00:00.000Z',
  scope: 'organization',
  kpis: { total: 2 },
  sections: {
    organizationOverview: {
      totalFarms: 2,
      totalPonds: 10,
      activePonds: 7,
      totalDevices: 20,
      activeAlerts: 3,
    },
    farmPerformanceTable: [
      {
        farmId: 'farm-1',
        farmName: 'Trại A',
        totalPonds: 6,
        activePonds: 5,
        activeAlerts: 2,
        onlineDeviceRate: 90,
      },
    ],
    farmRanking: [
      {
        farmId: 'farm-1',
        farmName: 'Trại A',
        totalPonds: 6,
        activePonds: 5,
        activeAlerts: 2,
        onlineDeviceRate: 90,
        score: 84,
      },
    ],
    alertCenter: [
      {
        id: 'alert-1',
        pondId: 'pond-1',
        pondName: 'Ao 1',
        type: 'oxygen_low',
        severity: 'high',
        message: 'DO thấp',
        status: 'active',
        createdAt: '2026-04-25T00:00:00.000Z',
      },
    ],
    organizationDeviceSummary: {
      total: 20,
      online: 18,
      offline: 1,
      error: 1,
      onlineRate: 90,
    },
    dataInterventionQueue: [
      {
        id: 'intervention-1',
        issueType: 'oxygen_low',
        target: 'Ao 1',
        suggestedAction: 'Kiểm tra và xử lý ngay theo SOP',
        detectedAt: '2026-04-25T00:00:00.000Z',
      },
    ],
    ownerStaffActivity: [
      {
        userId: 'u-1',
        fullName: 'Nguyễn Văn A',
        role: 'OWNER',
        activityCountLast7Days: 8,
        lastActivityAt: '2026-04-25T00:00:00.000Z',
      },
    ],
    reportsSnapshot: {
      activeCycles: 4,
      todayLogs: 12,
      growthRecordsLast7Days: 30,
    },
  },
});

describe('AdminDashboard', () => {
  it('renders all Admin sections and excludes Super Admin-only sections', async () => {
    useAdminDashboardMock.mockReturnValue({
      isLoading: false,
      isError: false,
      data: buildResponse(),
    });

    renderWithProviders(<AdminDashboard />);

    expect(await screen.findByText('Dashboard quản trị tổ chức')).toBeInTheDocument();

    expect(screen.getByText('Tổng quan tổ chức')).toBeInTheDocument();
    expect(screen.getByText('Bảng hiệu suất trang trại')).toBeInTheDocument();
    expect(screen.getByText('Xếp hạng trang trại')).toBeInTheDocument();
    expect(screen.getByText('Trung tâm cảnh báo')).toBeInTheDocument();
    expect(screen.getByText('Tình trạng thiết bị toàn tổ chức')).toBeInTheDocument();
    expect(screen.getByText('Hàng đợi can thiệp dữ liệu')).toBeInTheDocument();
    expect(screen.getByText('Hoạt động chủ trại và nhân sự')).toBeInTheDocument();
    expect(screen.getByText('Tóm tắt báo cáo vận hành')).toBeInTheDocument();

    expect(screen.queryByText(/System Health/i)).not.toBeInTheDocument();
    expect(screen.queryByText(/Audit Security/i)).not.toBeInTheDocument();
  });

  it('renders a clear empty state when organization has no farms', async () => {
    const emptyResponse = buildResponse();
    emptyResponse.sections.organizationOverview.totalFarms = 0;
    emptyResponse.sections.farmPerformanceTable = [];
    emptyResponse.sections.farmRanking = [];
    emptyResponse.sections.alertCenter = [];
    emptyResponse.sections.dataInterventionQueue = [];
    emptyResponse.sections.ownerStaffActivity = [];

    useAdminDashboardMock.mockReturnValue({
      isLoading: false,
      isError: false,
      data: emptyResponse,
    });

    renderWithProviders(<AdminDashboard />);

    expect(await screen.findByText('Tổ chức chưa có trang trại')).toBeInTheDocument();
    expect(screen.getByText('Vui lòng tạo trang trại để bắt đầu theo dõi dashboard.')).toBeInTheDocument();
  });
});
  • Step 2: Run test to verify it fails

Run:

bun --cwd frontend run test -- src/pages/dashboard/admin/AdminDashboard.spec.tsx

Expected: FAIL because current AdminDashboard.tsx is still placeholder and does not render required Vietnamese sections/empty state.

  • Step 3: Commit failing test
git add frontend/src/pages/dashboard/admin/AdminDashboard.spec.tsx
git commit -m "test: define admin dashboard phase 2.2 UI expectations"

Task 2: Upgrade Admin dashboard type contract

Files:

  • Modify: frontend/src/types/dashboard/admin-dashboard.types.ts

  • Test: frontend/src/pages/dashboard/admin/AdminDashboard.spec.tsx

  • Step 1: Write explicit Admin section types (no placeholders)

import type { DashboardBaseResponse } from '@/types/dashboard/dashboard-base.types';

export interface AdminOrganizationOverview {
  totalFarms: number;
  totalPonds: number;
  activePonds: number;
  totalDevices: number;
  activeAlerts: number;
}

export interface AdminFarmPerformanceItem {
  farmId: string;
  farmName: string;
  totalPonds: number;
  activePonds: number;
  activeAlerts: number;
  onlineDeviceRate: number;
}

export interface AdminFarmRankingItem extends AdminFarmPerformanceItem {
  score: number;
}

export interface AdminAlertCenterItem {
  id: string;
  pondId: string | null;
  pondName: string | null;
  type: string;
  severity: string;
  message: string;
  status: string;
  createdAt: string;
}

export interface AdminOrganizationDeviceSummary {
  total: number;
  online: number;
  offline: number;
  error: number;
  onlineRate: number;
}

export interface AdminDataInterventionItem {
  id: string;
  issueType: string;
  target: string;
  suggestedAction: string;
  detectedAt: string;
}

export interface AdminOwnerStaffActivityItem {
  userId: string;
  fullName: string;
  role: string;
  activityCountLast7Days: number;
  lastActivityAt: string | null;
}

export interface AdminReportsSnapshot {
  activeCycles: number;
  todayLogs: number;
  growthRecordsLast7Days: number;
}

export interface AdminDashboardSections {
  organizationOverview: AdminOrganizationOverview;
  farmPerformanceTable: AdminFarmPerformanceItem[];
  farmRanking: AdminFarmRankingItem[];
  alertCenter: AdminAlertCenterItem[];
  organizationDeviceSummary: AdminOrganizationDeviceSummary;
  dataInterventionQueue: AdminDataInterventionItem[];
  ownerStaffActivity: AdminOwnerStaffActivityItem[];
  reportsSnapshot: AdminReportsSnapshot;
}

export type AdminDashboardResponse = DashboardBaseResponse<AdminDashboardSections>;
  • Step 2: Run failing test again to keep RED state

Run:

bun --cwd frontend run test -- src/pages/dashboard/admin/AdminDashboard.spec.tsx

Expected: still FAIL (type contract is ready, UI composition not implemented yet).

  • Step 3: Commit type contract update
git add frontend/src/types/dashboard/admin-dashboard.types.ts
git commit -m "refactor: align admin dashboard frontend types with backend dto"

Task 3: Create Admin section components using design system

Files:

  • Create: frontend/src/pages/dashboard/admin/components/OrganizationOverview.tsx

  • Create: frontend/src/pages/dashboard/admin/components/OrganizationDeviceSummary.tsx

  • Create: frontend/src/pages/dashboard/admin/components/ReportsSnapshot.tsx

  • Create: frontend/src/pages/dashboard/admin/components/FarmPerformanceTable.tsx

  • Create: frontend/src/pages/dashboard/admin/components/FarmRanking.tsx

  • Create: frontend/src/pages/dashboard/admin/components/AdminAlertCenter.tsx

  • Create: frontend/src/pages/dashboard/admin/components/DataInterventionQueue.tsx

  • Create: frontend/src/pages/dashboard/admin/components/OwnerStaffActivity.tsx

  • Test: frontend/src/pages/dashboard/admin/AdminDashboard.spec.tsx

  • Step 1: Create OrganizationOverview.tsx

import { Card, Col, Row, Statistic } from 'antd';
import type { AdminOrganizationOverview } from '@/types/dashboard/admin-dashboard.types';

interface OrganizationOverviewProps {
  data: AdminOrganizationOverview;
}

export const OrganizationOverview = ({ data }: OrganizationOverviewProps) => (
  <Card title="Tổng quan tổ chức">
    <Row gutter={[12, 12]}>
      <Col xs={12} md={8}>
        <Statistic title="Tổng trang trại" value={data.totalFarms} />
      </Col>
      <Col xs={12} md={8}>
        <Statistic title="Tổng ao" value={data.totalPonds} />
      </Col>
      <Col xs={12} md={8}>
        <Statistic title="Ao hoạt động" value={data.activePonds} />
      </Col>
      <Col xs={12} md={8}>
        <Statistic title="Tổng thiết bị" value={data.totalDevices} />
      </Col>
      <Col xs={12} md={8}>
        <Statistic title="Cảnh báo active" value={data.activeAlerts} />
      </Col>
    </Row>
  </Card>
);
  • Step 2: Create OrganizationDeviceSummary.tsx
import { Card, Col, Row, Statistic } from 'antd';
import type { AdminOrganizationDeviceSummary } from '@/types/dashboard/admin-dashboard.types';

interface OrganizationDeviceSummaryProps {
  data: AdminOrganizationDeviceSummary;
}

export const OrganizationDeviceSummary = ({ data }: OrganizationDeviceSummaryProps) => (
  <Card title="Tình trạng thiết bị toàn tổ chức">
    <Row gutter={[12, 12]}>
      <Col xs={12} md={8}>
        <Statistic title="Tổng" value={data.total} />
      </Col>
      <Col xs={12} md={8}>
        <Statistic title="Online" value={data.online} />
      </Col>
      <Col xs={12} md={8}>
        <Statistic title="Offline" value={data.offline} />
      </Col>
      <Col xs={12} md={8}>
        <Statistic title="Error" value={data.error} />
      </Col>
      <Col xs={12} md={8}>
        <Statistic title="Tỷ lệ online (%)" value={data.onlineRate} />
      </Col>
    </Row>
  </Card>
);
  • Step 3: Create ReportsSnapshot.tsx
import { Card, Col, Row, Statistic } from 'antd';
import type { AdminReportsSnapshot } from '@/types/dashboard/admin-dashboard.types';

interface ReportsSnapshotProps {
  data: AdminReportsSnapshot;
}

export const ReportsSnapshot = ({ data }: ReportsSnapshotProps) => (
  <Card title="Tóm tắt báo cáo vận hành">
    <Row gutter={[12, 12]}>
      <Col xs={12} md={8}>
        <Statistic title="Chu kỳ active" value={data.activeCycles} />
      </Col>
      <Col xs={12} md={8}>
        <Statistic title="Nhật ký hôm nay" value={data.todayLogs} />
      </Col>
      <Col xs={12} md={8}>
        <Statistic title="Growth 7 ngày" value={data.growthRecordsLast7Days} />
      </Col>
    </Row>
  </Card>
);
  • Step 4: Create FarmPerformanceTable.tsx
import type { ColumnsType } from 'antd/es/table';
import { Card, Typography } from 'antd';
import { useMemo } from 'react';
import { DataTable } from '@/components/common/DataTable';
import type { AdminFarmPerformanceItem } from '@/types/dashboard/admin-dashboard.types';

interface FarmPerformanceTableProps {
  data: AdminFarmPerformanceItem[];
}

export const FarmPerformanceTable = ({ data }: FarmPerformanceTableProps) => {
  const columns = useMemo<ColumnsType<AdminFarmPerformanceItem>>(
    () => [
      { title: 'Trang trại', dataIndex: 'farmName', key: 'farmName' },
      { title: 'Tổng ao', dataIndex: 'totalPonds', key: 'totalPonds' },
      { title: 'Ao hoạt động', dataIndex: 'activePonds', key: 'activePonds' },
      { title: 'Cảnh báo active', dataIndex: 'activeAlerts', key: 'activeAlerts' },
      {
        title: 'Online device (%)',
        dataIndex: 'onlineDeviceRate',
        key: 'onlineDeviceRate',
        render: (value: number) => `${value}%`,
      },
    ],
    [],
  );

  return (
    <Card title="Bảng hiệu suất trang trại">
      <Typography.Text type="secondary">Hiển thị tối đa 10 trang trại</Typography.Text>
      <DataTable<AdminFarmPerformanceItem>
        columns={columns}
        dataSource={data}
        rowKey="farmId"
        pagination={false}
      />
    </Card>
  );
};
  • Step 5: Create FarmRanking.tsx
import type { ColumnsType } from 'antd/es/table';
import { Card, Typography } from 'antd';
import { useMemo } from 'react';
import { DataTable } from '@/components/common/DataTable';
import type { AdminFarmRankingItem } from '@/types/dashboard/admin-dashboard.types';

interface FarmRankingProps {
  data: AdminFarmRankingItem[];
}

type RankedFarm = AdminFarmRankingItem & { rank: number };

export const FarmRanking = ({ data }: FarmRankingProps) => {
  const rankedData = useMemo<RankedFarm[]>(
    () => data.map((item, index) => ({ ...item, rank: index + 1 })),
    [data],
  );

  const columns = useMemo<ColumnsType<RankedFarm>>(
    () => [
      { title: 'Hạng', dataIndex: 'rank', key: 'rank' },
      { title: 'Trang trại', dataIndex: 'farmName', key: 'farmName' },
      { title: 'Điểm', dataIndex: 'score', key: 'score' },
      { title: 'Ao hoạt động', dataIndex: 'activePonds', key: 'activePonds' },
      {
        title: 'Online device (%)',
        dataIndex: 'onlineDeviceRate',
        key: 'onlineDeviceRate',
        render: (value: number) => `${value}%`,
      },
    ],
    [],
  );

  return (
    <Card title="Xếp hạng trang trại">
      <Typography.Text type="secondary">Hiển thị tối đa 10 trang trại</Typography.Text>
      <DataTable<RankedFarm> columns={columns} dataSource={rankedData} rowKey="farmId" pagination={false} />
    </Card>
  );
};
  • Step 6: Create AdminAlertCenter.tsx
import type { ColumnsType } from 'antd/es/table';
import { Card, Tag } from 'antd';
import { useMemo } from 'react';
import { DataTable } from '@/components/common/DataTable';
import type { AdminAlertCenterItem } from '@/types/dashboard/admin-dashboard.types';

interface AdminAlertCenterProps {
  data: AdminAlertCenterItem[];
}

const mapSeverityColor = (severity: string) => {
  if (severity === 'high') return 'error';
  if (severity === 'medium') return 'warning';
  return 'default';
};

export const AdminAlertCenter = ({ data }: AdminAlertCenterProps) => {
  const columns = useMemo<ColumnsType<AdminAlertCenterItem>>(
    () => [
      { title: 'Ao', dataIndex: 'pondName', key: 'pondName', render: (value: string | null) => value ?? '-' },
      { title: 'Loại', dataIndex: 'type', key: 'type' },
      {
        title: 'Mức độ',
        dataIndex: 'severity',
        key: 'severity',
        render: (value: string) => <Tag color={mapSeverityColor(value)}>{value.toUpperCase()}</Tag>,
      },
      { title: 'Thông điệp', dataIndex: 'message', key: 'message' },
      { title: 'Trạng thái', dataIndex: 'status', key: 'status' },
      {
        title: 'Thời gian',
        dataIndex: 'createdAt',
        key: 'createdAt',
        render: (value: string) => new Date(value).toLocaleString('vi-VN'),
      },
    ],
    [],
  );

  return (
    <Card title="Trung tâm cảnh báo">
      <DataTable<AdminAlertCenterItem>
        columns={columns}
        dataSource={data}
        rowKey="id"
        pagination={{ pageSize: 10, showSizeChanger: false }}
      />
    </Card>
  );
};
  • Step 7: Create DataInterventionQueue.tsx
import type { ColumnsType } from 'antd/es/table';
import { Card } from 'antd';
import { useMemo } from 'react';
import { DataTable } from '@/components/common/DataTable';
import type { AdminDataInterventionItem } from '@/types/dashboard/admin-dashboard.types';

interface DataInterventionQueueProps {
  data: AdminDataInterventionItem[];
}

export const DataInterventionQueue = ({ data }: DataInterventionQueueProps) => {
  const columns = useMemo<ColumnsType<AdminDataInterventionItem>>(
    () => [
      { title: 'Loại vấn đề', dataIndex: 'issueType', key: 'issueType' },
      { title: 'Mục tiêu', dataIndex: 'target', key: 'target' },
      { title: 'Hành động gợi ý', dataIndex: 'suggestedAction', key: 'suggestedAction' },
      {
        title: 'Phát hiện lúc',
        dataIndex: 'detectedAt',
        key: 'detectedAt',
        render: (value: string) => new Date(value).toLocaleString('vi-VN'),
      },
    ],
    [],
  );

  return (
    <Card title="Hàng đợi can thiệp dữ liệu">
      <DataTable<AdminDataInterventionItem>
        columns={columns}
        dataSource={data}
        rowKey="id"
        pagination={{ pageSize: 10, showSizeChanger: false }}
      />
    </Card>
  );
};
  • Step 8: Create OwnerStaffActivity.tsx
import type { ColumnsType } from 'antd/es/table';
import { Card } from 'antd';
import { useMemo } from 'react';
import { DataTable } from '@/components/common/DataTable';
import type { AdminOwnerStaffActivityItem } from '@/types/dashboard/admin-dashboard.types';

interface OwnerStaffActivityProps {
  data: AdminOwnerStaffActivityItem[];
}

export const OwnerStaffActivity = ({ data }: OwnerStaffActivityProps) => {
  const columns = useMemo<ColumnsType<AdminOwnerStaffActivityItem>>(
    () => [
      { title: 'Họ tên', dataIndex: 'fullName', key: 'fullName' },
      { title: 'Vai trò', dataIndex: 'role', key: 'role' },
      {
        title: 'Hoạt động 7 ngày',
        dataIndex: 'activityCountLast7Days',
        key: 'activityCountLast7Days',
      },
      {
        title: 'Hoạt động gần nhất',
        dataIndex: 'lastActivityAt',
        key: 'lastActivityAt',
        render: (value: string | null) => (value ? new Date(value).toLocaleString('vi-VN') : '-'),
      },
    ],
    [],
  );

  return (
    <Card title="Hoạt động chủ trại và nhân sự">
      <DataTable<AdminOwnerStaffActivityItem>
        columns={columns}
        dataSource={data}
        rowKey="userId"
        pagination={{ pageSize: 10, showSizeChanger: false }}
      />
    </Card>
  );
};
  • Step 9: Run test to keep RED before page composition

Run:

bun --cwd frontend run test -- src/pages/dashboard/admin/AdminDashboard.spec.tsx

Expected: still FAIL until AdminDashboard.tsx composes these sections.

  • Step 10: Commit component scaffolding
git add frontend/src/pages/dashboard/admin/components
git commit -m "feat: add admin dashboard section components"

Task 4: Compose Admin dashboard page with design system (GREEN)

Files:

  • Modify: frontend/src/pages/dashboard/admin/AdminDashboard.tsx

  • Test: frontend/src/pages/dashboard/admin/AdminDashboard.spec.tsx

  • Step 1: Replace placeholder page with architecture-first composition

import { Col, Row } from 'antd';
import { EmptyState } from '@/components/common/EmptyState';
import { LoadingSkeleton } from '@/components/common/LoadingSkeleton';
import { PageHeader } from '@/components/common/PageHeader';
import { StatsCard } from '@/components/common/StatsCard';
import { useAdminDashboard } from '@/hooks/dashboard/useAdminDashboard';
import { AdminAlertCenter } from '@/pages/dashboard/admin/components/AdminAlertCenter';
import { DataInterventionQueue } from '@/pages/dashboard/admin/components/DataInterventionQueue';
import { FarmPerformanceTable } from '@/pages/dashboard/admin/components/FarmPerformanceTable';
import { FarmRanking } from '@/pages/dashboard/admin/components/FarmRanking';
import { OrganizationDeviceSummary } from '@/pages/dashboard/admin/components/OrganizationDeviceSummary';
import { OrganizationOverview } from '@/pages/dashboard/admin/components/OrganizationOverview';
import { OwnerStaffActivity } from '@/pages/dashboard/admin/components/OwnerStaffActivity';
import { ReportsSnapshot } from '@/pages/dashboard/admin/components/ReportsSnapshot';

export const AdminDashboard = () => {
  const query = useAdminDashboard();

  if (query.isLoading) {
    return <LoadingSkeleton type="card" />;
  }

  if (query.isError || !query.data) {
    return (
      <EmptyState
        title="Lỗi tải dashboard quản trị"
        description="Không thể tải dữ liệu dashboard admin. Vui lòng thử lại sau."
      />
    );
  }

  const { sections } = query.data;

  if (sections.organizationOverview.totalFarms === 0) {
    return (
      <div>
        <PageHeader
          title="Dashboard quản trị tổ chức"
          subtitle="Theo dõi vận hành đa trang trại trong tổ chức"
        />
        <EmptyState
          title="Tổ chức chưa có trang trại"
          description="Vui lòng tạo trang trại để bắt đầu theo dõi dashboard."
        />
      </div>
    );
  }

  return (
    <div>
      <PageHeader
        title="Dashboard quản trị tổ chức"
        subtitle="Theo dõi vận hành đa trang trại trong tổ chức"
      />

      <Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
        <Col xs={24} sm={12} lg={6}>
          <StatsCard title="Tổng trang trại" value={sections.organizationOverview.totalFarms} color="blue" />
        </Col>
        <Col xs={24} sm={12} lg={6}>
          <StatsCard title="Tổng ao" value={sections.organizationOverview.totalPonds} color="green" />
        </Col>
        <Col xs={24} sm={12} lg={6}>
          <StatsCard title="Cảnh báo active" value={sections.organizationOverview.activeAlerts} color="red" />
        </Col>
        <Col xs={24} sm={12} lg={6}>
          <StatsCard title="Tỷ lệ online (%)" value={sections.organizationDeviceSummary.onlineRate} color="blue" />
        </Col>
      </Row>

      <Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
        <Col xs={24} xl={12}>
          <OrganizationOverview data={sections.organizationOverview} />
        </Col>
        <Col xs={24} xl={12}>
          <OrganizationDeviceSummary data={sections.organizationDeviceSummary} />
        </Col>
        <Col xs={24}>
          <ReportsSnapshot data={sections.reportsSnapshot} />
        </Col>
      </Row>

      <Row gutter={[16, 16]}>
        <Col xs={24}>
          <FarmPerformanceTable data={sections.farmPerformanceTable} />
        </Col>
        <Col xs={24}>
          <FarmRanking data={sections.farmRanking} />
        </Col>
        <Col xs={24}>
          <AdminAlertCenter data={sections.alertCenter} />
        </Col>
        <Col xs={24}>
          <DataInterventionQueue data={sections.dataInterventionQueue} />
        </Col>
        <Col xs={24}>
          <OwnerStaffActivity data={sections.ownerStaffActivity} />
        </Col>
      </Row>
    </div>
  );
};
  • Step 2: Run test to verify GREEN

Run:

bun --cwd frontend run test -- src/pages/dashboard/admin/AdminDashboard.spec.tsx

Expected: PASS.

  • Step 3: Commit page composition
git add frontend/src/pages/dashboard/admin/AdminDashboard.tsx
git commit -m "feat: implement admin dashboard page with design-system composition"

Task 5: Refactor and verify broader dashboard stability

Files:

  • Modify (if needed by Biome autofix): files from Tasks 1-4

  • Test: frontend/src/pages/dashboard/**/*.spec.tsx

  • Step 1: Run dashboard test suite

Run:

bun --cwd frontend run test -- src/pages/dashboard

Expected: PASS for DashboardRoute, OwnerDashboard, StaffDashboard, and new AdminDashboard tests.

  • Step 2: Run repository-wide check

Run:

bun run check

Expected: PASS with no Biome errors.

  • Step 3: Commit verification-safe formatting adjustments
git add frontend/src/pages/dashboard/admin frontend/src/types/dashboard/admin-dashboard.types.ts
git commit -m "test: verify admin dashboard phase 2.2 integration and formatting"

Acceptance Mapping

  • Admin sees required sections: enforced by AdminDashboard.spec.tsx first test.
  • Large tables have pagination or visible limit: FarmPerformanceTable/FarmRanking show visible max-10 note and non-paginated limited data; operational tables use pagination config.
  • No Super Admin-only system sections: first test asserts absence of system/audit labels.
  • Empty state present: second test asserts organization-no-farms empty state messaging.
  • No any: explicit interface typing across all new/modified files.
  • Frontend alias imports only: all snippets use @/ imports.

Spec Self-Review

  1. Spec coverage: Phase 2.2 Admin UI sections, table behavior, empty state, role-sensitive absence, and check command all mapped to tasks.
  2. Placeholder scan: No TODO/TBD placeholders in tasks or code snippets.
  3. Type consistency: AdminDashboardResponse/section item names are consistent between tests, page, and section components.

Plan complete and saved to docs/superpowers/plans/2026-04-25-admin-dashboard-phase-2-2-design-system.md. Two execution options:

1. Subagent-Driven (recommended) - I dispatch a fresh subagent per task, review between tasks, fast iteration

2. Inline Execution - Execute tasks in this session using executing-plans, batch execution with checkpoints

Which approach?