feat: add vehicle count column to admin user management
All checks were successful
Deploy to Staging / Build Images (push) Successful in 4m34s
Deploy to Staging / Deploy to Staging (push) Successful in 37s
Deploy to Staging / Verify Staging (push) Successful in 6s
Deploy to Staging / Notify Staging Ready (push) Successful in 5s
Deploy to Staging / Notify Staging Failure (push) Has been skipped

Add a new "Vehicles" column to the admin user management table showing
the count of active vehicles for each user.

Backend changes:
- Add vehicleCount to UserWithAdminStatus type
- Add SQL subquery to count active vehicles (is_active=true, not deleted)
- Add vehicleCount as sortable column option

Frontend changes:
- Add Vehicles column to desktop table (between Tier and Status)
- Add VehicleCountBadge component to mobile user cards
- Update ManagedUser type with vehicleCount field

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-01-01 15:23:23 -06:00
parent aa441b185f
commit 0b16b8307f
5 changed files with 27 additions and 5 deletions

View File

@@ -176,6 +176,7 @@ export class UserProfileRepository {
...this.mapRowToUserProfile(row), ...this.mapRowToUserProfile(row),
isAdmin: !!row.admin_auth0_sub, isAdmin: !!row.admin_auth0_sub,
adminRole: row.admin_role || null, adminRole: row.admin_role || null,
vehicleCount: parseInt(row.vehicle_count, 10) || 0,
}; };
} }
@@ -196,6 +197,7 @@ export class UserProfileRepository {
createdAt: 'up.created_at', createdAt: 'up.created_at',
displayName: 'up.display_name', displayName: 'up.display_name',
subscriptionTier: 'up.subscription_tier', subscriptionTier: 'up.subscription_tier',
vehicleCount: 'vehicle_count',
}; };
const sortColumn = sortColumnMap[sortBy] || 'up.created_at'; const sortColumn = sortColumnMap[sortBy] || 'up.created_at';
@@ -234,14 +236,18 @@ export class UserProfileRepository {
${whereClause} ${whereClause}
`; `;
// Data query with admin status join // Data query with admin status join and vehicle count
const dataQuery = ` const dataQuery = `
SELECT SELECT
up.id, up.auth0_sub, up.email, up.display_name, up.notification_email, up.id, up.auth0_sub, up.email, up.display_name, up.notification_email,
up.subscription_tier, up.email_verified, up.onboarding_completed_at, up.subscription_tier, up.email_verified, up.onboarding_completed_at,
up.deactivated_at, up.deactivated_by, up.created_at, up.updated_at, up.deactivated_at, up.deactivated_by, up.created_at, up.updated_at,
au.auth0_sub as admin_auth0_sub, au.auth0_sub as admin_auth0_sub,
au.role as admin_role au.role as admin_role,
(SELECT COUNT(*) FROM vehicles v
WHERE v.user_id = up.auth0_sub
AND v.is_active = true
AND v.deleted_at IS NULL) as vehicle_count
FROM user_profiles up FROM user_profiles up
LEFT JOIN admin_users au ON up.auth0_sub = au.auth0_sub AND au.revoked_at IS NULL LEFT JOIN admin_users au ON up.auth0_sub = au.auth0_sub AND au.revoked_at IS NULL
${whereClause} ${whereClause}
@@ -275,7 +281,11 @@ export class UserProfileRepository {
up.subscription_tier, up.email_verified, up.onboarding_completed_at, up.subscription_tier, up.email_verified, up.onboarding_completed_at,
up.deactivated_at, up.deactivated_by, up.created_at, up.updated_at, up.deactivated_at, up.deactivated_by, up.created_at, up.updated_at,
au.auth0_sub as admin_auth0_sub, au.auth0_sub as admin_auth0_sub,
au.role as admin_role au.role as admin_role,
(SELECT COUNT(*) FROM vehicles v
WHERE v.user_id = up.auth0_sub
AND v.is_active = true
AND v.deleted_at IS NULL) as vehicle_count
FROM user_profiles up FROM user_profiles up
LEFT JOIN admin_users au ON up.auth0_sub = au.auth0_sub AND au.revoked_at IS NULL LEFT JOIN admin_users au ON up.auth0_sub = au.auth0_sub AND au.revoked_at IS NULL
WHERE up.auth0_sub = $1 WHERE up.auth0_sub = $1

View File

@@ -27,6 +27,7 @@ export interface UserProfile {
export interface UserWithAdminStatus extends UserProfile { export interface UserWithAdminStatus extends UserProfile {
isAdmin: boolean; isAdmin: boolean;
adminRole: 'admin' | 'super_admin' | null; adminRole: 'admin' | 'super_admin' | null;
vehicleCount: number;
} }
// Pagination and filter query params for listing users // Pagination and filter query params for listing users
@@ -36,7 +37,7 @@ export interface ListUsersQuery {
search?: string; search?: string;
tier?: SubscriptionTier; tier?: SubscriptionTier;
status?: 'active' | 'deactivated' | 'all'; status?: 'active' | 'deactivated' | 'all';
sortBy?: 'email' | 'createdAt' | 'displayName' | 'subscriptionTier'; sortBy?: 'email' | 'createdAt' | 'displayName' | 'subscriptionTier' | 'vehicleCount';
sortOrder?: 'asc' | 'desc'; sortOrder?: 'asc' | 'desc';
} }

View File

@@ -81,6 +81,13 @@ const StatusBadge: React.FC<{ active: boolean }> = ({ active }) => (
</span> </span>
); );
// Vehicle count badge component
const VehicleCountBadge: React.FC<{ count: number }> = ({ count }) => (
<span className="px-2 py-1 rounded-full text-xs font-medium bg-slate-100 text-slate-700">
{count} {count === 1 ? 'vehicle' : 'vehicles'}
</span>
);
export const AdminUsersMobileScreen: React.FC = () => { export const AdminUsersMobileScreen: React.FC = () => {
const { isAdmin, loading: adminLoading } = useAdminAccess(); const { isAdmin, loading: adminLoading } = useAdminAccess();
@@ -453,6 +460,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
)} )}
<div className="flex flex-wrap gap-2 mt-2"> <div className="flex flex-wrap gap-2 mt-2">
<TierBadge tier={user.subscriptionTier} /> <TierBadge tier={user.subscriptionTier} />
<VehicleCountBadge count={user.vehicleCount} />
<StatusBadge active={!user.deactivatedAt} /> <StatusBadge active={!user.deactivatedAt} />
{user.isAdmin && ( {user.isAdmin && (
<span className="px-2 py-1 rounded-full text-xs font-medium bg-amber-100 text-amber-700"> <span className="px-2 py-1 rounded-full text-xs font-medium bg-amber-100 text-amber-700">

View File

@@ -237,6 +237,7 @@ export interface ManagedUser {
updatedAt: string; updatedAt: string;
isAdmin: boolean; isAdmin: boolean;
adminRole: 'admin' | 'super_admin' | null; adminRole: 'admin' | 'super_admin' | null;
vehicleCount: number;
} }
// List users response with pagination // List users response with pagination
@@ -254,7 +255,7 @@ export interface ListUsersParams {
search?: string; search?: string;
tier?: SubscriptionTier; tier?: SubscriptionTier;
status?: 'active' | 'deactivated' | 'all'; status?: 'active' | 'deactivated' | 'all';
sortBy?: 'email' | 'createdAt' | 'displayName' | 'subscriptionTier'; sortBy?: 'email' | 'createdAt' | 'displayName' | 'subscriptionTier' | 'vehicleCount';
sortOrder?: 'asc' | 'desc'; sortOrder?: 'asc' | 'desc';
} }

View File

@@ -420,6 +420,7 @@ export const AdminUsersPage: React.FC = () => {
<TableCell>Email</TableCell> <TableCell>Email</TableCell>
<TableCell>Display Name</TableCell> <TableCell>Display Name</TableCell>
<TableCell>Tier</TableCell> <TableCell>Tier</TableCell>
<TableCell>Vehicles</TableCell>
<TableCell>Status</TableCell> <TableCell>Status</TableCell>
<TableCell>Admin</TableCell> <TableCell>Admin</TableCell>
<TableCell>Created</TableCell> <TableCell>Created</TableCell>
@@ -450,6 +451,7 @@ export const AdminUsersPage: React.FC = () => {
</Select> </Select>
</FormControl> </FormControl>
</TableCell> </TableCell>
<TableCell>{user.vehicleCount}</TableCell>
<TableCell> <TableCell>
<Chip <Chip
label={user.deactivatedAt ? 'Deactivated' : 'Active'} label={user.deactivatedAt ? 'Deactivated' : 'Active'}