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
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:
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
Reference in New Issue
Block a user