feat: extract admin account management rework from PR 124 (#138)

* feat: reorganize admin panel around accounts

* feat: expand admin customer account controls

* feat: add admin account detail management

* fix: remove unused admin account icon import

* refactor: design critique fixes for account pages

- Admin accounts: replace dense card grid with compact DataTable
- Account settings: remove redundant hero card, stat grid, header pills
- Fix bg-accent (orange) misuse on decorative elements across 7 files
- Add ConfirmButton for destructive actions (deactivate, remove member)
- Replace single-field modals with inline editing (plan, trial)
- Add contextual help: display code tooltip, improved empty states
- Non-owner aside explanation for hidden owner-only sections
- Admin sidebar: group 11 items into 5 labeled sections
- Rename UsersPage.tsx → AccountsPage.tsx to match route
- Fix border radius consistency, hide zero-count badges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use get_admin_db for all new admin account endpoints

All admin endpoints query across tenants without a tenant context.
get_db (app-role, subject to RLS) was never imported and would crash
at runtime — replace all 6 occurrences with get_admin_db (BYPASSRLS).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #138.
This commit is contained in:
chihlasm
2026-04-13 04:44:51 -04:00
committed by GitHub
parent abd79bc763
commit a71f082e25
18 changed files with 3141 additions and 1213 deletions

View File

@@ -19,8 +19,116 @@ class TestAdminEndpoints:
"/api/v1/admin/users", headers=admin_auth_headers
)
assert response.status_code == 200
users = response.json()
assert len(users) >= 2 # admin + test_user
payload = response.json()
assert payload["total"] >= 2 # admin + test_user
assert len(payload["items"]) >= 2
@pytest.mark.asyncio
async def test_list_users_supports_search(
self, client: AsyncClient, admin_auth_headers: dict, test_user: dict
):
"""Test admin people search by user email."""
response = await client.get(
"/api/v1/admin/users",
params={"search": test_user["email"]},
headers=admin_auth_headers,
)
assert response.status_code == 200
payload = response.json()
assert payload["total"] >= 1
assert any(item["email"] == test_user["email"] for item in payload["items"])
@pytest.mark.asyncio
async def test_list_accounts_as_admin(
self, client: AsyncClient, admin_auth_headers: dict
):
"""Test listing accounts with member data."""
response = await client.get(
"/api/v1/admin/accounts", headers=admin_auth_headers
)
assert response.status_code == 200
payload = response.json()
assert payload["total"] >= 1
assert len(payload["items"]) >= 1
assert "members" in payload["items"][0]
assert "subscription" in payload["items"][0]
@pytest.mark.asyncio
async def test_create_account_as_admin(
self, client: AsyncClient, admin_auth_headers: dict
):
"""Test creating an empty account from admin."""
response = await client.post(
"/api/v1/admin/accounts",
json={"name": "Acme Customer", "plan": "pro"},
headers=admin_auth_headers,
)
assert response.status_code == 201
payload = response.json()
assert payload["name"] == "Acme Customer"
assert payload["subscription"]["plan"] == "pro"
assert payload["display_code"]
@pytest.mark.asyncio
async def test_get_account_detail_as_admin(
self, client: AsyncClient, admin_auth_headers: dict, test_user: dict
):
"""Test fetching account detail for management view."""
account_id = test_user["user_data"]["account_id"]
response = await client.get(
f"/api/v1/admin/accounts/{account_id}",
headers=admin_auth_headers,
)
assert response.status_code == 200
payload = response.json()
assert payload["id"] == account_id
assert "members" in payload
assert "invites" in payload
@pytest.mark.asyncio
async def test_update_account_name_as_admin(
self, client: AsyncClient, admin_auth_headers: dict, test_user: dict
):
"""Test renaming an account from admin detail view."""
account_id = test_user["user_data"]["account_id"]
response = await client.put(
f"/api/v1/admin/accounts/{account_id}",
json={"name": "Renamed Customer Account"},
headers=admin_auth_headers,
)
assert response.status_code == 200
payload = response.json()
assert payload["id"] == account_id
assert payload["name"] == "Renamed Customer Account"
@pytest.mark.asyncio
async def test_update_account_plan(
self, client: AsyncClient, admin_auth_headers: dict, test_user: dict
):
"""Test changing an account's subscription plan."""
account_id = test_user["user_data"]["account_id"]
response = await client.put(
f"/api/v1/admin/accounts/{account_id}/subscription/plan",
json={"plan": "pro"},
headers=admin_auth_headers,
)
assert response.status_code == 200
assert response.json()["plan"] == "pro"
@pytest.mark.asyncio
async def test_extend_account_trial(
self, client: AsyncClient, admin_auth_headers: dict, test_user: dict
):
"""Test starting or extending an account trial."""
account_id = test_user["user_data"]["account_id"]
response = await client.put(
f"/api/v1/admin/accounts/{account_id}/subscription/extend-trial",
json={"days": 14},
headers=admin_auth_headers,
)
assert response.status_code == 200
assert response.json()["status"] == "trialing"
assert response.json()["current_period_end"] is not None
@pytest.mark.asyncio
async def test_list_users_as_non_admin(