diff --git a/maternal-app/maternal-app-backend/src/main.ts b/maternal-app/maternal-app-backend/src/main.ts index 9a4bc5f..5cebdff 100644 --- a/maternal-app/maternal-app-backend/src/main.ts +++ b/maternal-app/maternal-app-backend/src/main.ts @@ -57,8 +57,10 @@ async function bootstrap() { 'http://localhost:3001', // Next.js dev (legacy) 'http://localhost:3030', // Next.js dev (current) 'http://localhost:3005', // Next.js dev (port 3005) + 'http://localhost:3335', // Admin dashboard dev 'https://maternal.noru1.ro', // Production frontend (legacy) 'https://maternal-api.noru1.ro', // Production API (legacy) + 'https://pfadmin.noru1.ro', // Production admin dashboard 'https://web.parentflowapp.com', // Production frontend 'https://api.parentflowapp.com', // Production API (for GraphQL playground) ]; diff --git a/maternal-app/maternal-app-backend/src/modules/admin/user-management/user-management.controller.ts b/maternal-app/maternal-app-backend/src/modules/admin/user-management/user-management.controller.ts index 271892d..ce429b1 100644 --- a/maternal-app/maternal-app-backend/src/modules/admin/user-management/user-management.controller.ts +++ b/maternal-app/maternal-app-backend/src/modules/admin/user-management/user-management.controller.ts @@ -21,7 +21,7 @@ import { PaginatedUsersResponseDto, } from './user-management.dto'; -@Controller('admin/users') +@Controller('api/v1/admin/users') @UseGuards(AdminGuard) export class UserManagementController { constructor(private readonly userManagementService: UserManagementService) {} diff --git a/maternal-app/maternal-app-backend/src/modules/admin/user-management/user-management.dto.ts b/maternal-app/maternal-app-backend/src/modules/admin/user-management/user-management.dto.ts index 1c7ad9d..f6fe29c 100644 --- a/maternal-app/maternal-app-backend/src/modules/admin/user-management/user-management.dto.ts +++ b/maternal-app/maternal-app-backend/src/modules/admin/user-management/user-management.dto.ts @@ -114,6 +114,9 @@ export class UserResponseDto { emailVerified: boolean; createdAt: Date; updatedAt: Date; + familyCount?: number; + childrenCount?: number; + deviceCount?: number; } export class PaginatedUsersResponseDto { diff --git a/maternal-app/maternal-app-backend/src/modules/admin/user-management/user-management.service.ts b/maternal-app/maternal-app-backend/src/modules/admin/user-management/user-management.service.ts index faac19e..0eabb2a 100644 --- a/maternal-app/maternal-app-backend/src/modules/admin/user-management/user-management.service.ts +++ b/maternal-app/maternal-app-backend/src/modules/admin/user-management/user-management.service.ts @@ -54,8 +54,12 @@ export class UserManagementService { .take(limit) .getManyAndCount(); + // Fetch counts for all users in a single query + const userIds = users.map((u) => u.id); + const counts = await this.getUserCounts(userIds); + return { - users: users.map((user) => this.toResponseDto(user)), + users: users.map((user) => this.toResponseDto(user, counts[user.id])), total, page, limit, @@ -63,6 +67,67 @@ export class UserManagementService { }; } + private async getUserCounts(userIds: string[]): Promise< + Record< + string, + { familyCount: number; childrenCount: number; deviceCount: number } + > + > { + if (userIds.length === 0) return {}; + + // Query family memberships + const familyCountsRaw = await this.userRepository.query( + `SELECT user_id, COUNT(DISTINCT family_id) as count + FROM family_members + WHERE user_id = ANY($1) + GROUP BY user_id`, + [userIds], + ); + + // Query children (through families they belong to) + const childrenCountsRaw = await this.userRepository.query( + `SELECT fm.user_id, COUNT(DISTINCT c.id) as count + FROM family_members fm + JOIN children c ON c.family_id = fm.family_id + WHERE fm.user_id = ANY($1) + GROUP BY fm.user_id`, + [userIds], + ); + + // Query devices + const deviceCountsRaw = await this.userRepository.query( + `SELECT user_id, COUNT(*) as count + FROM device_registry + WHERE user_id = ANY($1) + GROUP BY user_id`, + [userIds], + ); + + // Build counts map + const counts: Record< + string, + { familyCount: number; childrenCount: number; deviceCount: number } + > = {}; + + userIds.forEach((userId) => { + counts[userId] = { familyCount: 0, childrenCount: 0, deviceCount: 0 }; + }); + + familyCountsRaw.forEach((row: any) => { + counts[row.user_id].familyCount = parseInt(row.count, 10); + }); + + childrenCountsRaw.forEach((row: any) => { + counts[row.user_id].childrenCount = parseInt(row.count, 10); + }); + + deviceCountsRaw.forEach((row: any) => { + counts[row.user_id].deviceCount = parseInt(row.count, 10); + }); + + return counts; + } + async getUserById(userId: string): Promise { const user = await this.userRepository.findOne({ where: { id: userId }, @@ -72,7 +137,8 @@ export class UserManagementService { throw new NotFoundException(`User with ID ${userId} not found`); } - return this.toResponseDto(user); + const counts = await this.getUserCounts([userId]); + return this.toResponseDto(user, counts[userId]); } async createUser(dto: CreateUserDto): Promise { @@ -158,7 +224,10 @@ export class UserManagementService { }; } - private toResponseDto(user: User): UserResponseDto { + private toResponseDto( + user: User, + counts?: { familyCount: number; childrenCount: number; deviceCount: number }, + ): UserResponseDto { return { id: user.id, email: user.email, @@ -171,6 +240,9 @@ export class UserManagementService { emailVerified: user.emailVerified, createdAt: user.createdAt, updatedAt: user.updatedAt, + familyCount: counts?.familyCount, + childrenCount: counts?.childrenCount, + deviceCount: counts?.deviceCount, }; } } diff --git a/parentflow-admin/src/app/users/page.tsx b/parentflow-admin/src/app/users/page.tsx index dc10d5d..dad363c 100644 --- a/parentflow-admin/src/app/users/page.tsx +++ b/parentflow-admin/src/app/users/page.tsx @@ -44,12 +44,19 @@ interface User { id: string; email: string; name: string; + phone?: string; + photoUrl?: string; + globalRole: 'parent' | 'guest' | 'admin'; + isAdmin: boolean; + adminPermissions: string[]; + emailVerified: boolean; createdAt: string; - lastActiveAt: string; - isActive: boolean; - familyCount: number; - childrenCount: number; - deviceCount: number; + updatedAt: string; + lastActiveAt?: string; + isActive?: boolean; + familyCount?: number; + childrenCount?: number; + deviceCount?: number; } export default function UsersPage() { @@ -70,45 +77,16 @@ export default function UsersPage() { try { setLoading(true); const response = await apiClient.get('/admin/users'); - setUsers(response.data); - } catch (error) { + // Backend returns paginated response: { users: [], total, page, limit, totalPages } + setUsers(response.users || []); + } catch (error: any) { console.error('Failed to fetch users:', error); - // Using mock data for development - setUsers([ - { - id: '1', - email: 'john.doe@example.com', - name: 'John Doe', - createdAt: '2024-01-15T10:00:00Z', - lastActiveAt: '2024-10-06T08:30:00Z', - isActive: true, - familyCount: 1, - childrenCount: 2, - deviceCount: 3, - }, - { - id: '2', - email: 'jane.smith@example.com', - name: 'Jane Smith', - createdAt: '2024-02-20T14:30:00Z', - lastActiveAt: '2024-10-05T18:45:00Z', - isActive: true, - familyCount: 1, - childrenCount: 1, - deviceCount: 2, - }, - { - id: '3', - email: 'bob.johnson@example.com', - name: 'Bob Johnson', - createdAt: '2024-03-10T09:15:00Z', - lastActiveAt: '2024-09-30T12:00:00Z', - isActive: false, - familyCount: 1, - childrenCount: 3, - deviceCount: 1, - }, - ]); + // If unauthorized, redirect to login + if (error?.response?.status === 401) { + window.location.href = '/login'; + return; + } + setUsers([]); } finally { setLoading(false); } @@ -127,7 +105,7 @@ export default function UsersPage() { const handleToggleUserStatus = async (user: User) => { try { await apiClient.patch(`/admin/users/${user.id}`, { - isActive: !user.isActive, + emailVerified: !user.emailVerified, }); fetchUsers(); } catch (error) { @@ -174,56 +152,48 @@ export default function UsersPage() { {/* Stats Cards */} - - - - - - Total Users - - - {users.length} - - - - - - - - - Active Users - - - {users.filter(u => u.isActive).length} - - - - - - - - - Total Families - - - {users.reduce((sum, u) => sum + u.familyCount, 0)} - - - - - - - - - Total Children - - - {users.reduce((sum, u) => sum + u.childrenCount, 0)} - - - - - + + + + + Total Users + + + {users.length} + + + + + + + Active Users + + + {users.filter(u => u.emailVerified).length} + + + + + + + Total Families + + + {users.reduce((sum, u) => sum + (u.familyCount || 0), 0)} + + + + + + + Total Children + + + {users.reduce((sum, u) => sum + (u.childrenCount || 0), 0)} + + + + {/* Search and Actions */} @@ -285,18 +255,18 @@ export default function UsersPage() { {formatDate(user.createdAt)} - {formatDate(user.lastActiveAt)} + {user.lastActiveAt ? formatDate(user.lastActiveAt) : formatDate(user.updatedAt)} : } + icon={user.emailVerified ? : } /> - {user.familyCount} - {user.childrenCount} - {user.deviceCount} + {user.familyCount || 0} + {user.childrenCount || 0} + {user.deviceCount || 0} handleToggleUserStatus(user)} - title={user.isActive ? 'Deactivate' : 'Activate'} + title={user.emailVerified ? 'Deactivate' : 'Activate'} > - {user.isActive ? : } + {user.emailVerified ? : } User Details {selectedUser && ( - - - - - Name - - {selectedUser.name} - - - - Email - - {selectedUser.email} - - - - User ID - - {selectedUser.id} - - - - Status - - - - - - Created At - - - {formatDate(selectedUser.createdAt)} - - - - - Last Active - - - {formatDate(selectedUser.lastActiveAt)} - - - + + + + Name + + {selectedUser.name} + + + + Email + + {selectedUser.email} + + + + User ID + + {selectedUser.id} + + + + Status + + + + + + Created At + + + {formatDate(selectedUser.createdAt)} + + + + + Last Active + + + {selectedUser.lastActiveAt ? formatDate(selectedUser.lastActiveAt) : formatDate(selectedUser.updatedAt)} + + )} @@ -434,9 +402,9 @@ export default function UsersPage() { /> + } - label="Active" + label="Email Verified" /> )} diff --git a/parentflow-admin/src/lib/api-client.ts b/parentflow-admin/src/lib/api-client.ts index abefb73..e03341b 100644 --- a/parentflow-admin/src/lib/api-client.ts +++ b/parentflow-admin/src/lib/api-client.ts @@ -35,7 +35,7 @@ class ApiClient { private async request(method: string, endpoint: string, data?: any, options?: any) { const config = { method, - url: `${API_BASE_URL}/api/v1${endpoint}`, + url: `${API_BASE_URL}${endpoint}`, headers: { 'Content-Type': 'application/json', ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}), @@ -55,7 +55,7 @@ class ApiClient { // Handle token refresh if (error.response?.status === 401 && this.refreshToken) { try { - const refreshResponse = await axios.post(`${API_BASE_URL}/api/v1/auth/refresh`, { + const refreshResponse = await axios.post(`${API_BASE_URL}/auth/refresh`, { refreshToken: this.refreshToken, }); @@ -75,23 +75,73 @@ class ApiClient { } } + // Generic HTTP methods + async get(endpoint: string, options?: any) { + return this.request('GET', endpoint, undefined, options); + } + + async post(endpoint: string, data?: any, options?: any) { + return this.request('POST', endpoint, data, options); + } + + async patch(endpoint: string, data?: any, options?: any) { + return this.request('PATCH', endpoint, data, options); + } + + async put(endpoint: string, data?: any, options?: any) { + return this.request('PUT', endpoint, data, options); + } + + async delete(endpoint: string, options?: any) { + return this.request('DELETE', endpoint, undefined, options); + } + // Auth endpoints async login(email: string, password: string) { - const response = await this.request('POST', '/admin/auth/login', { email, password }); - this.setTokens(response.accessToken, response.refreshToken); + // Generate device info for admin dashboard + const deviceInfo = { + deviceId: this.getOrCreateDeviceId(), + platform: 'web', + model: 'Admin Dashboard', + osVersion: navigator.userAgent, + }; + + const response = await this.request('POST', '/auth/login', { + email, + password, + deviceInfo + }); + + // Extract tokens from nested response structure + const tokens = response.tokens || response.data?.tokens; + if (tokens?.accessToken && tokens?.refreshToken) { + this.setTokens(tokens.accessToken, tokens.refreshToken); + } + return response; } + private getOrCreateDeviceId(): string { + if (typeof window === 'undefined') return 'server'; + + let deviceId = localStorage.getItem('admin_device_id'); + if (!deviceId) { + deviceId = 'admin_' + Math.random().toString(36).substring(2) + Date.now().toString(36); + localStorage.setItem('admin_device_id', deviceId); + } + return deviceId; + } + async logout() { try { - await this.request('POST', '/admin/auth/logout'); + await this.request('POST', '/auth/logout'); } finally { this.clearTokens(); } } async getCurrentAdmin() { - return this.request('GET', '/admin/auth/me'); + return this.request('GET', '/auth/me'); } // User management endpoints