Skip to content

Commit a7ec71f

Browse files
authored
[PT2025BMHW-110][SuperAdminAPI,SuperAdminUI,Infrastructure] Implement tenant list sorting and filtering (#148)
1 parent 7656e7c commit a7ec71f

File tree

7 files changed

+58
-43
lines changed

7 files changed

+58
-43
lines changed

Frontend/apps/superadmin/src/components/tenants/TenantsPage.tsx

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Tenant, TenantSortField, TenantStatus } from '@ngo-platform/shared
22
import { Alert, Badge, Button, Card, CardContent, CardHeader, CardTitle, Icon, Spinner, features } from '@ngo-platform/shared';
33
import { type FC, useEffect, useState } from 'react';
44
import { useNavigate } from 'react-router-dom';
5+
import { CreateTenantDialog } from './CreateTenantDialog';
56

67
const ITEMS_PER_PAGE = 10;
78

@@ -18,6 +19,9 @@ export const TenantsPage: FC = () => {
1819
page: currentPage,
1920
pageSize: ITEMS_PER_PAGE,
2021
search: searchQuery || undefined,
22+
isActive: statusFilter === 'All' ? undefined : statusFilter === 'Active',
23+
sortBy: sortField,
24+
sortOrder: sortOrder,
2125
});
2226

2327
const tenants = tenantResponse?.items ?? [];
@@ -81,6 +85,11 @@ export const TenantsPage: FC = () => {
8185
// TODO: Show confirmation dialog and delete
8286
};
8387

88+
const handleCreateSuccess = () => {
89+
setIsCreateDialogOpen(false);
90+
// Optionally refresh the list or show a success message
91+
};
92+
8493
const goToPage = (page: number) => {
8594
if (page >= 1 && page <= totalPages) {
8695
setCurrentPage(page);
@@ -166,23 +175,6 @@ export const TenantsPage: FC = () => {
166175
</CardContent>
167176
</Card>
168177

169-
<Card className="border-l-4 border-l-warning-500">
170-
<CardContent className="p-4">
171-
<div className="flex items-center justify-between">
172-
<div>
173-
<p className="text-sm text-neutral-600 dark:text-neutral-400">Suspended</p>
174-
<p className="text-2xl font-bold text-neutral-900 dark:text-white">
175-
{/* TODO: Backend does not provide suspended status yet, only isActive */}
176-
{tenants.filter(t => !t.isActive && t.status === 'Suspended').length}
177-
</p>
178-
</div>
179-
<div className="w-12 h-12 bg-warning-100 dark:bg-warning-900/30 rounded-full flex items-center justify-center">
180-
<Icon name="alertTriangle" size={24} className="text-warning-600 dark:text-warning-400" />
181-
</div>
182-
</div>
183-
</CardContent>
184-
</Card>
185-
186178
<Card className="border-l-4 border-l-warning-500">
187179
<CardContent className="p-4">
188180
<div className="flex items-center justify-between">

Frontend/apps/superadmin/src/pages/DashboardPage.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
Icon,
66
useAuth,
77
useLogout,
8-
config
8+
config,
9+
features
910
} from '@ngo-platform/shared';
1011
import { DashboardContent } from '../components/dashboard';
1112
import { TenantsPage } from '../components/tenants';
@@ -22,6 +23,12 @@ export const DashboardPage: FC = () => {
2223
const { mutate: logout } = useLogout(config.apis.superAdminApi);
2324
const { user } = useAuth();
2425

26+
// Fetch tenant count for sidebar badge
27+
const { data: tenantResponse } = features.useTenants({
28+
page: 1,
29+
pageSize: 1, // Only need count, not actual data
30+
});
31+
2532
// Update URL when page changes
2633
useEffect(() => {
2734
setSearchParams({ page: activePage }, { replace: true });
@@ -115,9 +122,11 @@ export const DashboardPage: FC = () => {
115122
>
116123
<Icon name="building" size={20} />
117124
<span className="font-medium">Tenants</span>
118-
<span className="ml-auto px-2 py-0.5 text-xs font-semibold bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 rounded-full">
119-
24
120-
</span>
125+
{tenantResponse?.totalCount !== undefined && (
126+
<span className="ml-auto px-2 py-0.5 text-xs font-semibold bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 rounded-full">
127+
{tenantResponse.totalCount}
128+
</span>
129+
)}
121130
</button>
122131

123132
<button

Frontend/packages/shared/src/config/index.ts

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -51,39 +51,24 @@ const getBaseUrl = (): string => {
5151
return 'localhost';
5252
};
5353

54-
/**
55-
* Check if running in React Native environment
56-
*/
57-
const isReactNative = (): boolean => {
58-
return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
59-
};
60-
6154
/**
6255
* Get configuration based on current environment and platform
6356
*/
6457
export const getConfig = (): PlatformConfig => {
6558
const environment = getEnvironment();
6659
const baseUrl = getBaseUrl();
67-
const isMobile = isReactNative();
68-
69-
// Use HTTP for mobile development to avoid SSL issues with self-signed certificates
70-
// Web apps continue to use HTTPS
71-
const protocol = isMobile && environment === 'development' ? 'http' : 'https';
72-
73-
// Map ports: HTTPS (500x) -> HTTP (508x) for mobile development
74-
const getPort = (basePort: number) => isMobile && environment === 'development' ? basePort : basePort;
7560

7661
const configs: Record<typeof environment, PlatformConfig> = {
7762
development: {
7863
apis: {
79-
sharedApi: `${protocol}://${baseUrl}:${getPort(5000)}`,
80-
superAdminApi: `${protocol}://${baseUrl}:${getPort(5001)}`,
81-
tenantAdminApi: `${protocol}://${baseUrl}:${getPort(5002)}`,
82-
volunteerApi: `${protocol}://${baseUrl}:${getPort(5003)}`,
64+
sharedApi: `https://${baseUrl}:5000`,
65+
superAdminApi: `https://${baseUrl}:5001`,
66+
tenantAdminApi: `https://${baseUrl}:5002`,
67+
volunteerApi: `https://${baseUrl}:5003`,
8368
},
8469
apps: {
85-
superAdmin: 'https://localhost:3001',
86-
tenantAdmin: 'https://localhost:3000',
70+
superAdmin: 'http://localhost:3001',
71+
tenantAdmin: 'http://localhost:3000',
8772
},
8873
environment: 'development',
8974
},

Frontend/packages/shared/src/features/tenants/queries/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export function getTenantsQueryOptions(
5252
pageSize: params?.pageSize,
5353
searchTerm: params?.search,
5454
isActive: params?.isActive,
55+
sortBy: params?.sortBy,
56+
sortOrder: params?.sortOrder,
5557
},
5658
});
5759

Infrastructure/NGOPlatform.AppHost/appsettings.Development.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
"AllowedOrigins": [
1616
"https://localhost:3001",
1717
"http://localhost:3001",
18+
"http://localhost:5174",
19+
"https://localhost:5174",
1820
"https://localhost:3002",
1921
"http://localhost:3002",
2022
"https://localhost:3003",
@@ -25,6 +27,8 @@
2527
"AllowedOrigins": [
2628
"https://localhost:3000",
2729
"http://localhost:3000",
30+
"http://localhost:5173",
31+
"https://localhost:5173",
2832
"https://localhost:3002",
2933
"http://localhost:3002",
3034
"https://localhost:3003",

Infrastructure/NGOPlatform.SuperAdmin/NGOPlatform.SuperAdmin.Api/Controllers/TenantsController.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,18 @@ public async Task<IActionResult> GetAll(
3636
[FromQuery] int pageSize = 10,
3737
[FromQuery] string? searchTerm = null,
3838
[FromQuery] bool? isActive = null,
39+
[FromQuery] string? sortBy = "createdAt",
40+
[FromQuery] string? sortOrder = "desc",
3941
CancellationToken cancellationToken = default)
4042
{
4143
var query = new GetAllTenantsQuery
4244
{
4345
PageNumber = pageNumber,
4446
PageSize = pageSize,
4547
SearchTerm = searchTerm,
46-
IsActive = isActive
48+
IsActive = isActive,
49+
SortBy = sortBy,
50+
SortOrder = sortOrder
4751
};
4852

4953
var result = await _mediator.Send(query, cancellationToken);

Infrastructure/NGOPlatform.SuperAdmin/NGOPlatform.SuperAdmin.Application/Queries/Tenants/GetAllTenantsQuery.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public partial record GetAllTenantsQuery : IQuery<PaginatedList<TenantListItemDt
1616
public int PageSize { get; init; } = 10;
1717
public string? SearchTerm { get; init; }
1818
public bool? IsActive { get; init; }
19+
public string? SortBy { get; init; } = "createdAt";
20+
public string? SortOrder { get; init; } = "desc";
1921
}
2022

2123
/// <summary>
@@ -54,13 +56,30 @@ public async Task<PaginatedList<TenantListItemDto>> Handle(GetAllTenantsQuery re
5456
CreatedAt = tenant.CreatedAt
5557
}).ToList();
5658

59+
// Apply sorting in-memory (for now)
60+
dtos = ApplySorting(dtos, request.SortBy, request.SortOrder);
61+
5762
var result = new PaginatedList<TenantListItemDto>(dtos, totalCount, request.PageNumber, request.PageSize);
5863

5964
_logger.LogInformation("Retrieved {Count} tenants (page {PageNumber} of {TotalPages})",
6065
dtos.Count, request.PageNumber, result.TotalPages);
6166

6267
return result;
6368
}
69+
70+
private static List<TenantListItemDto> ApplySorting(List<TenantListItemDto> tenants, string? sortBy, string? sortOrder)
71+
{
72+
var isDescending = sortOrder?.ToLowerInvariant() == "desc";
73+
74+
return (sortBy?.ToLowerInvariant()) switch
75+
{
76+
"name" => isDescending ? tenants.OrderByDescending(t => t.Name).ToList() : tenants.OrderBy(t => t.Name).ToList(),
77+
"subdomain" => isDescending ? tenants.OrderByDescending(t => t.Subdomain).ToList() : tenants.OrderBy(t => t.Subdomain).ToList(),
78+
"isactive" => isDescending ? tenants.OrderByDescending(t => t.IsActive).ToList() : tenants.OrderBy(t => t.IsActive).ToList(),
79+
"maxusers" => isDescending ? tenants.OrderByDescending(t => t.MaxUsers).ToList() : tenants.OrderBy(t => t.MaxUsers).ToList(),
80+
"createdat" or _ => isDescending ? tenants.OrderByDescending(t => t.CreatedAt).ToList() : tenants.OrderBy(t => t.CreatedAt).ToList(),
81+
};
82+
}
6483
}
6584

6685
/// <summary>

0 commit comments

Comments
 (0)