Design: Organization Device Management Tab

Overview

Add a “Quản lý thiết bị” tab to the Organization Detail page. This tab lets Super Admins manage devices scoped to a specific organization. The tab reuses the existing DeviceList structure with org-filtered data and role-based permission gating.


Scope

Phase 1 only:

  • CRUD devices that belong to the current organization (list, create, edit, delete)
  • Devices not yet assigned to any organization are NOT included — only devices already belonging to this organization
  • Permission matrix:
    • Super Admin: full CRUD + assign/unassign
    • Admin / Owner: read-only (view list only, no create/edit/delete buttons)

Out of scope for Phase 1:

  • Bulk import/export
  • Device firmware updates
  • Device pairing/wiring to ponds (future)

Architecture

File Changes

File Change
frontend/src/pages/organizations/OrganizationDetail.tsx Add new tab item to <Tabs>
frontend/src/pages/organizations/components/OrganizationDevicesTab.tsx New — tab component
frontend/src/pages/organizations/components/OrganizationDevicesTab.spec.tsx New — test

Data Flow

OrganizationDetail (tab key: 'devices')
  → <OrganizationDevicesTab organization={organization} />useDevices({ orgId: organization.id })   ← TanStack Query, filter by org
      → deviceService.createDevice({ ...data, organizationId: org.id })  ← auto-assign org
      → permission check via getOrganizationUserCapabilities

Key Decisions

  1. Filter on orgId: Backend GET /devices?orgId={id} must return only devices belonging to this organization. This is validated at the service layer via OrganizationFilterInterceptor.
  2. Auto-assign on create: When Super Admin creates a device via this tab, organizationId is set to organization.id before the API call — the form does not expose an org selector.
  3. Permission gating: Button visibility (Create/Edit/Delete) is controlled by tabCapabilities derived from getOrganizationUserCapabilities({ actorRole, selectedOrganizationId: organization.id }).
  4. Reuse pattern: Follows the same pattern as OrganizationUsersTab — same capability pattern, same form structure.

Components

OrganizationDevicesTab

Props:

interface OrganizationDevicesTabProps {
  organization: Organization;
}

State:

  • page, limit, search — for list query
  • drawerVisible — toggle create/edit drawer
  • selectedDevice — device being edited (or undefined for create)
  • filters{ type?, status? } for table filter

Behavior:

  • Fetch: useDevices({ orgId: organization.id, page, limit, search, type, status })
  • Create: deviceService.createDevice({ ...data, organizationId: organization.id })
  • Edit: deviceService.updateDevice(selectedDevice.id, data)
  • Delete: deviceService.deleteDevice(id)

Permission-gated UI:

const tabCaps = getOrganizationUserCapabilities({ actorRole, actorOrganizationId, selectedOrganizationId: organization.id });

// Only Super Admin can see action buttons
{tabCaps.canEditUser && <Button icon={<EditOutlined />} onClick={() => handleEdit(device)} />}
{tabCaps.canDeleteUser && <Popconfirm onConfirm={() => handleDelete(device.id)}>...}

{tabCaps.canCreateUser && <Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>Thêm thiết bị</Button>}

Note: canEditUser and canDeleteUser apply to device actions in this context. The naming follows the existing getOrganizationUserCapabilities pattern which is resource-agnostic. If needed, a dedicated canManageDevices flag can be added to the capability logic — but for Phase 1, we reuse the existing permission flags.


Form (Drawer)

Fields:

  • name (required, string)
  • serialNumber (required, string)
  • type (required, enum: sensor / gateway / inverter / feeder)
  • model (required, string)

Hidden fields (auto-set):

  • organizationId = organization.id

On submit:

  • Validate with Zod schema
  • Call deviceService.createDevice or deviceService.updateDevice
  • Close drawer, refetch list

UI Consistency

  • Follows OrganizationUsersTab layout pattern
  • Uses PageHeader pattern but stripped (no PageHeader in tab — just header block)
  • Uses DataTable for table rendering
  • Status badge colors via statusColors from @/theme/colors
  • Ant Design Tag, Button, Drawer consistent with rest of app

Testing Strategy

Unit tests (OrganizationDevicesTab.spec.tsx):

  • Super Admin sees Create button, Edit/Delete on rows
  • Admin sees no action buttons
  • Owner sees no action buttons
  • Create form submits with correct organizationId
  • Edit form pre-populates fields
  • Delete shows confirmation and calls service
  • Empty state shown when no devices

Integration tests (future):

  • End-to-end flow via Super Admin creating device in org → device appears in list