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

View File

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

View File

@@ -81,6 +81,13 @@ const StatusBadge: React.FC<{ active: boolean }> = ({ active }) => (
</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 = () => {
const { isAdmin, loading: adminLoading } = useAdminAccess();
@@ -453,6 +460,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
)}
<div className="flex flex-wrap gap-2 mt-2">
<TierBadge tier={user.subscriptionTier} />
<VehicleCountBadge count={user.vehicleCount} />
<StatusBadge active={!user.deactivatedAt} />
{user.isAdmin && (
<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;
isAdmin: boolean;
adminRole: 'admin' | 'super_admin' | null;
vehicleCount: number;
}
// List users response with pagination
@@ -254,7 +255,7 @@ export interface ListUsersParams {
search?: string;
tier?: SubscriptionTier;
status?: 'active' | 'deactivated' | 'all';
sortBy?: 'email' | 'createdAt' | 'displayName' | 'subscriptionTier';
sortBy?: 'email' | 'createdAt' | 'displayName' | 'subscriptionTier' | 'vehicleCount';
sortOrder?: 'asc' | 'desc';
}

View File

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