diff --git a/lib/controller/dashboard/dashboard_controller.dart b/lib/controller/dashboard/dashboard_controller.dart index 89074b3..91d812a 100644 --- a/lib/controller/dashboard/dashboard_controller.dart +++ b/lib/controller/dashboard/dashboard_controller.dart @@ -10,7 +10,6 @@ class DashboardController extends GetxController { // ========================= final RxList> roleWiseData = >[].obs; - final RxString attendanceSelectedRange = '15D'.obs; final RxBool attendanceIsChartView = true.obs; final RxBool isAttendanceLoading = false.obs; @@ -19,11 +18,31 @@ class DashboardController extends GetxController { // Project progress overview // ========================= final RxList projectChartData = [].obs; - final RxString projectSelectedRange = '15D'.obs; final RxBool projectIsChartView = true.obs; final RxBool isProjectLoading = false.obs; + // ========================= + // Projects overview + // ========================= + final RxInt totalProjects = 0.obs; + final RxInt ongoingProjects = 0.obs; + final RxBool isProjectsLoading = false.obs; + + // ========================= + // Tasks overview + // ========================= + final RxInt totalTasks = 0.obs; + final RxInt completedTasks = 0.obs; + final RxBool isTasksLoading = false.obs; + + // ========================= + // Teams overview + // ========================= + final RxInt totalEmployees = 0.obs; + final RxInt inToday = 0.obs; + final RxBool isTeamsLoading = false.obs; + // Common ranges final List ranges = ['7D', '15D', '30D']; @@ -39,30 +58,21 @@ class DashboardController extends GetxController { level: LogLevel.info, ); - if (projectController.selectedProjectId.value.isNotEmpty) { - fetchRoleWiseAttendance(); - fetchProjectProgress(); - } + fetchAllDashboardData(); // React to project change ever(projectController.selectedProjectId, (id) { - if (id.isNotEmpty) { - logSafe('Project changed to $id, refreshing dashboard', - level: LogLevel.info); - fetchRoleWiseAttendance(); - fetchProjectProgress(); - } + fetchAllDashboardData(); }); - // 👇 Separate listeners for ranges + // React to range changes ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance()); ever(projectSelectedRange, (_) => fetchProjectProgress()); } - // ========================= - // Helpers - // ========================= - + /// ========================= + /// Helper Methods + /// ========================= int _getDaysFromRange(String range) { switch (range) { case '7D': @@ -72,9 +82,9 @@ class DashboardController extends GetxController { case '30D': return 30; case '3M': - return 90; + return 90; case '6M': - return 180; + return 180; default: return 7; } @@ -104,36 +114,49 @@ class DashboardController extends GetxController { logSafe('Project chart view toggled to: $isChart', level: LogLevel.debug); } + /// ========================= /// Manual refresh + /// ========================= Future refreshDashboard() async { logSafe('Manual dashboard refresh triggered.', level: LogLevel.debug); - await Future.wait([ - fetchRoleWiseAttendance(), - fetchProjectProgress(), - ]); + await fetchAllDashboardData(); } - // ========================= - // API Calls - // ========================= - - Future fetchRoleWiseAttendance() async { + /// ========================= + /// Fetch all dashboard data + /// ========================= + Future fetchAllDashboardData() async { final String projectId = projectController.selectedProjectId.value; + // Skip fetching if no project is selected if (projectId.isEmpty) { - logSafe('Project ID is empty, skipping attendance API call.', + logSafe('No project selected. Skipping dashboard API calls.', level: LogLevel.warning); return; } + await Future.wait([ + fetchRoleWiseAttendance(), + fetchProjectProgress(), + fetchDashboardTasks(projectId: projectId), + fetchDashboardTeams(projectId: projectId), + ]); + } + + /// ========================= + /// API Calls + /// ========================= + Future fetchRoleWiseAttendance() async { + final String projectId = projectController.selectedProjectId.value; + + if (projectId.isEmpty) return; + try { isAttendanceLoading.value = true; final List? response = await ApiService.getDashboardAttendanceOverview( - projectId, - getAttendanceDays(), - ); + projectId, getAttendanceDays()); if (response != null) { roleWiseData.value = @@ -147,12 +170,8 @@ class DashboardController extends GetxController { } } catch (e, st) { roleWiseData.clear(); - logSafe( - 'Error fetching attendance overview', - level: LogLevel.error, - error: e, - stackTrace: st, - ); + logSafe('Error fetching attendance overview', + level: LogLevel.error, error: e, stackTrace: st); } finally { isAttendanceLoading.value = false; } @@ -161,21 +180,14 @@ class DashboardController extends GetxController { Future fetchProjectProgress() async { final String projectId = projectController.selectedProjectId.value; - if (projectId.isEmpty) { - logSafe('Project ID is empty, skipping project progress API call.', - level: LogLevel.warning); - return; - } + if (projectId.isEmpty) return; try { isProjectLoading.value = true; - // Instead of using DateTime.now(), we simply pass 'days' to the API - // Let the backend decide the correct date range final response = await ApiService.getProjectProgress( projectId: projectId, days: getProjectDays(), - // Remove fromDate ); if (response != null && response.success) { @@ -188,14 +200,64 @@ class DashboardController extends GetxController { } } catch (e, st) { projectChartData.clear(); - logSafe( - 'Error fetching project progress', - level: LogLevel.error, - error: e, - stackTrace: st, - ); + logSafe('Error fetching project progress', + level: LogLevel.error, error: e, stackTrace: st); } finally { isProjectLoading.value = false; } } + + Future fetchDashboardTasks({required String projectId}) async { + if (projectId.isEmpty) return; // Skip if empty + + try { + isTasksLoading.value = true; + + final response = await ApiService.getDashboardTasks(projectId: projectId); + + if (response != null && response.success) { + totalTasks.value = response.data?.totalTasks ?? 0; + completedTasks.value = response.data?.completedTasks ?? 0; + logSafe('Dashboard tasks fetched', level: LogLevel.info); + } else { + totalTasks.value = 0; + completedTasks.value = 0; + logSafe('Failed to fetch tasks', level: LogLevel.error); + } + } catch (e, st) { + totalTasks.value = 0; + completedTasks.value = 0; + logSafe('Error fetching tasks', + level: LogLevel.error, error: e, stackTrace: st); + } finally { + isTasksLoading.value = false; + } + } + + Future fetchDashboardTeams({required String projectId}) async { + if (projectId.isEmpty) return; // Skip if empty + + try { + isTeamsLoading.value = true; + + final response = await ApiService.getDashboardTeams(projectId: projectId); + + if (response != null && response.success) { + totalEmployees.value = response.data?.totalEmployees ?? 0; + inToday.value = response.data?.inToday ?? 0; + logSafe('Dashboard teams fetched', level: LogLevel.info); + } else { + totalEmployees.value = 0; + inToday.value = 0; + logSafe('Failed to fetch teams', level: LogLevel.error); + } + } catch (e, st) { + totalEmployees.value = 0; + inToday.value = 0; + logSafe('Error fetching teams', + level: LogLevel.error, error: e, stackTrace: st); + } finally { + isTeamsLoading.value = false; + } + } } diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 3b09a34..9bf8e59 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -5,6 +5,10 @@ class ApiEndpoints { // Dashboard Module API Endpoints static const String getDashboardAttendanceOverview = "/dashboard/attendance-overview"; static const String getDashboardProjectProgress = "/dashboard/progression"; + static const String getDashboardTasks = "/dashboard/tasks"; + static const String getDashboardTeams = "/dashboard/teams"; + static const String getDashboardProjects = "/dashboard/projects"; + // Attendance Module API Endpoints static const String getProjects = "/project/list"; diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index c8c54be..4c64820 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -9,6 +9,8 @@ import 'package:marco/helpers/services/api_endpoints.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:jwt_decoder/jwt_decoder.dart'; import 'package:marco/model/dashboard/project_progress_model.dart'; +import 'package:marco/model/dashboard/dashboard_tasks_model.dart'; +import 'package:marco/model/dashboard/dashboard_teams_model.dart'; import 'package:marco/helpers/services/app_logger.dart'; class ApiService { @@ -611,6 +613,73 @@ class ApiService { } // === Dashboard Endpoints === + /// Get Dashboard Tasks + static Future getDashboardTasks( + {required String projectId}) async { + try { + final queryParams = {'projectId': projectId}; + + final response = await _getRequest(ApiEndpoints.getDashboardTasks, + queryParams: queryParams); + + if (response == null || response.body.trim().isEmpty) { + logSafe("Dashboard tasks request failed or response empty", + level: LogLevel.error); + return null; + } + + final jsonResponse = jsonDecode(response.body); + if (jsonResponse is Map && + jsonResponse['success'] == true) { + logSafe( + "Dashboard tasks fetched successfully: ${jsonResponse['data']}"); + return DashboardTasks.fromJson(jsonResponse); + } else { + logSafe( + "Failed to fetch dashboard tasks: ${jsonResponse['message'] ?? 'Unknown error'}", + level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during getDashboardTasks API: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + return null; + } + + /// Get Dashboard Teams + static Future getDashboardTeams( + {required String projectId}) async { + try { + final queryParams = {'projectId': projectId}; + + final response = await _getRequest(ApiEndpoints.getDashboardTeams, + queryParams: queryParams); + + if (response == null || response.body.trim().isEmpty) { + logSafe("Dashboard teams request failed or response empty", + level: LogLevel.error); + return null; + } + + final jsonResponse = jsonDecode(response.body); + if (jsonResponse is Map && + jsonResponse['success'] == true) { + logSafe( + "Dashboard teams fetched successfully: ${jsonResponse['data']}"); + return DashboardTeams.fromJson(jsonResponse); + } else { + logSafe( + "Failed to fetch dashboard teams: ${jsonResponse['message'] ?? 'Unknown error'}", + level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during getDashboardTeams API: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + return null; + } static Future?> getDashboardAttendanceOverview( String projectId, int days) async { diff --git a/lib/helpers/widgets/dashbaord/dashboard_overview_widgets.dart b/lib/helpers/widgets/dashbaord/dashboard_overview_widgets.dart new file mode 100644 index 0000000..c9b5ff5 --- /dev/null +++ b/lib/helpers/widgets/dashbaord/dashboard_overview_widgets.dart @@ -0,0 +1,274 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/widgets/my_card.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/controller/dashboard/dashboard_controller.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; // import MyText + +class DashboardOverviewWidgets { + static final DashboardController dashboardController = + Get.find(); + + static const _titleTextStyle = TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: Colors.black87, + ); + + static const _subtitleTextStyle = TextStyle( + fontSize: 14, + color: Colors.grey, + ); + + static const _infoNumberTextStyle = TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black87, + ); + + static const _infoNumberGreenTextStyle = TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ); + + /// Teams Overview Card without chart, labels & values in rows + static Widget teamsOverview() { + return Obx(() { + if (dashboardController.isTeamsLoading.value) { + return _loadingSkeletonCard("Teams"); + } + + final total = dashboardController.totalEmployees.value; + final inToday = dashboardController.inToday.value; + + return LayoutBuilder( + builder: (context, constraints) { + final cardWidth = constraints.maxWidth > 400 + ? (constraints.maxWidth / 2) - 10 + : constraints.maxWidth; + + return SizedBox( + width: cardWidth, + child: MyCard( + borderRadiusAll: 16, + paddingAll: 20, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.group, + color: Colors.blueAccent, size: 26), + MySpacing.width(8), + MyText("Teams", style: _titleTextStyle), + ], + ), + MySpacing.height(16), + // Labels in one row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText("Total Employees", style: _subtitleTextStyle), + MyText("In Today", style: _subtitleTextStyle), + ], + ), + MySpacing.height(4), + // Values in one row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText(total.toString(), style: _infoNumberTextStyle), + MyText(inToday.toString(), + style: _infoNumberGreenTextStyle.copyWith( + color: Colors.green[700])), + ], + ), + ], + ), + ), + ); + }, + ); + }); + } + + /// Tasks Overview Card + static Widget tasksOverview() { + return Obx(() { + if (dashboardController.isTasksLoading.value) { + return _loadingSkeletonCard("Tasks"); + } + + final total = dashboardController.totalTasks.value; + final completed = dashboardController.completedTasks.value; + final remaining = total - completed; + final double percent = total > 0 ? completed / total : 0.0; + + // Task colors + const completedColor = Color(0xFFE57373); // red + const remainingColor = Color(0xFF64B5F6); // blue + + final List<_ChartData> pieData = [ + _ChartData('Completed', completed.toDouble(), completedColor), + _ChartData('Remaining', remaining.toDouble(), remainingColor), + ]; + + return LayoutBuilder( + builder: (context, constraints) { + final cardWidth = + constraints.maxWidth < 300 ? constraints.maxWidth : 300.0; + + return SizedBox( + width: cardWidth, + child: MyCard( + borderRadiusAll: 16, + paddingAll: 20, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Icon + Title + Row( + children: [ + const Icon(Icons.task_alt, + color: completedColor, size: 26), + MySpacing.width(8), + MyText("Tasks", style: _titleTextStyle), + ], + ), + MySpacing.height(16), + + // Main Row: Bigger Pie Chart + Full-Color Info Boxes + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Pie Chart Column (Bigger) + SizedBox( + height: 140, + width: 140, + child: SfCircularChart( + annotations: [ + CircularChartAnnotation( + widget: MyText( + "${(percent * 100).toInt()}%", + style: _infoNumberGreenTextStyle.copyWith( + fontSize: 20), + ), + ), + ], + series: >[ + PieSeries<_ChartData, String>( + dataSource: pieData, + xValueMapper: (_ChartData data, _) => + data.category, + yValueMapper: (_ChartData data, _) => data.value, + pointColorMapper: (_ChartData data, _) => + data.color, + dataLabelSettings: + const DataLabelSettings(isVisible: false), + radius: '100%', + ), + ], + ), + ), + MySpacing.width(16), + + // Info Boxes Column (Full Color) + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _infoBoxFullColor( + "Completed", completed, completedColor), + MySpacing.height(8), + _infoBoxFullColor( + "Remaining", remaining, remainingColor), + ], + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + }); + } + + /// Full-color info box + static Widget _infoBoxFullColor(String label, int value, Color bgColor) { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + decoration: BoxDecoration( + color: bgColor, // full color + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + MyText(value.toString(), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, // text in white for contrast + )), + MySpacing.height(2), + MyText(label, + style: const TextStyle( + fontSize: 12, + color: Colors.white, // text in white for contrast + )), + ], + ), + ); + } + + /// Loading Skeleton Card + static Widget _loadingSkeletonCard(String title) { + return LayoutBuilder(builder: (context, constraints) { + final cardWidth = + constraints.maxWidth < 200 ? constraints.maxWidth : 200.0; + + return SizedBox( + width: cardWidth, + child: MyCard( + borderRadiusAll: 16, + paddingAll: 20, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _loadingBar(width: 100), + MySpacing.height(12), + _loadingBar(width: 80), + MySpacing.height(12), + _loadingBar(width: double.infinity, height: 12), + ], + ), + ), + ); + }); + } + + static Widget _loadingBar( + {double width = double.infinity, double height = 16}) { + return Container( + height: height, + width: width, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(6), + ), + ); + } +} + +class _ChartData { + final String category; + final double value; + final Color color; + + _ChartData(this.category, this.value, this.color); +} diff --git a/lib/model/dashboard/dashboard_projects_model.dart b/lib/model/dashboard/dashboard_projects_model.dart new file mode 100644 index 0000000..5dce781 --- /dev/null +++ b/lib/model/dashboard/dashboard_projects_model.dart @@ -0,0 +1,46 @@ +// dashboard_projects_model.dart +class DashboardProjects { + final bool success; + final String message; + final DashboardProjectsData? data; + final dynamic errors; + final int statusCode; + final DateTime timestamp; + + DashboardProjects({ + required this.success, + required this.message, + required this.data, + this.errors, + required this.statusCode, + required this.timestamp, + }); + + factory DashboardProjects.fromJson(Map json) { + return DashboardProjects( + success: json['success'] ?? false, + message: json['message'] ?? '', + data: json['data'] != null ? DashboardProjectsData.fromJson(json['data']) : null, + errors: json['errors'], + statusCode: json['statusCode'] ?? 0, + timestamp: DateTime.parse(json['timestamp']), + ); + } +} + +class DashboardProjectsData { + final int totalProjects; + final int ongoingProjects; + + DashboardProjectsData({ + required this.totalProjects, + required this.ongoingProjects, + }); + + factory DashboardProjectsData.fromJson(Map json) { + return DashboardProjectsData( + totalProjects: json['totalProjects'] ?? 0, + ongoingProjects: json['ongoingProjects'] ?? 0, + ); + } +} diff --git a/lib/model/dashboard/dashboard_tasks_model.dart b/lib/model/dashboard/dashboard_tasks_model.dart new file mode 100644 index 0000000..ba10a7a --- /dev/null +++ b/lib/model/dashboard/dashboard_tasks_model.dart @@ -0,0 +1,46 @@ +// dashboard_tasks_model.dart +class DashboardTasks { + final bool success; + final String message; + final DashboardTasksData? data; + final dynamic errors; + final int statusCode; + final DateTime timestamp; + + DashboardTasks({ + required this.success, + required this.message, + required this.data, + this.errors, + required this.statusCode, + required this.timestamp, + }); + + factory DashboardTasks.fromJson(Map json) { + return DashboardTasks( + success: json['success'] ?? false, + message: json['message'] ?? '', + data: json['data'] != null ? DashboardTasksData.fromJson(json['data']) : null, + errors: json['errors'], + statusCode: json['statusCode'] ?? 0, + timestamp: DateTime.parse(json['timestamp']), + ); + } +} + +class DashboardTasksData { + final int totalTasks; + final int completedTasks; + + DashboardTasksData({ + required this.totalTasks, + required this.completedTasks, + }); + + factory DashboardTasksData.fromJson(Map json) { + return DashboardTasksData( + totalTasks: json['totalTasks'] ?? 0, + completedTasks: json['completedTasks'] ?? 0, + ); + } +} diff --git a/lib/model/dashboard/dashboard_teams_model.dart b/lib/model/dashboard/dashboard_teams_model.dart new file mode 100644 index 0000000..a2a81bb --- /dev/null +++ b/lib/model/dashboard/dashboard_teams_model.dart @@ -0,0 +1,46 @@ +// dashboard_teams_model.dart +class DashboardTeams { + final bool success; + final String message; + final DashboardTeamsData? data; + final dynamic errors; + final int statusCode; + final DateTime timestamp; + + DashboardTeams({ + required this.success, + required this.message, + required this.data, + this.errors, + required this.statusCode, + required this.timestamp, + }); + + factory DashboardTeams.fromJson(Map json) { + return DashboardTeams( + success: json['success'] ?? false, + message: json['message'] ?? '', + data: json['data'] != null ? DashboardTeamsData.fromJson(json['data']) : null, + errors: json['errors'], + statusCode: json['statusCode'] ?? 0, + timestamp: DateTime.parse(json['timestamp']), + ); + } +} + +class DashboardTeamsData { + final int totalEmployees; + final int inToday; + + DashboardTeamsData({ + required this.totalEmployees, + required this.inToday, + }); + + factory DashboardTeamsData.fromJson(Map json) { + return DashboardTeamsData( + totalEmployees: json['totalEmployees'] ?? 0, + inToday: json['inToday'] ?? 0, + ); + } +} diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index 1e810f8..a6a0d3a 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -14,6 +14,7 @@ import 'package:marco/helpers/widgets/dashbaord/project_progress_chart.dart'; import 'package:marco/view/layouts/layout.dart'; import 'package:marco/controller/dynamicMenu/dynamic_menu_controller.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; +import 'package:marco/helpers/widgets/dashbaord/dashboard_overview_widgets.dart'; // import 'package:marco/helpers/services/firebase/firebase_messaging_service.dart'; // ❌ Commented out @@ -77,6 +78,16 @@ class _DashboardScreenState extends State with UIMixin { MySpacing.height(10), */ _buildDashboardStats(context), + MySpacing.height(24), + SizedBox( + width: double.infinity, + child: DashboardOverviewWidgets.teamsOverview(), + ), + MySpacing.height(24), + SizedBox( + width: double.infinity, + child: DashboardOverviewWidgets.tasksOverview(), + ), MySpacing.height(24), _buildAttendanceChartSection(), MySpacing.height(24),