diff --git a/lib/controller/dashboard/attendance_screen_controller.dart b/lib/controller/attendance/attendance_screen_controller.dart similarity index 100% rename from lib/controller/dashboard/attendance_screen_controller.dart rename to lib/controller/attendance/attendance_screen_controller.dart diff --git a/lib/controller/dashboard/dashboard_controller.dart b/lib/controller/dashboard/dashboard_controller.dart index 2ead7d1..91d812a 100644 --- a/lib/controller/dashboard/dashboard_controller.dart +++ b/lib/controller/dashboard/dashboard_controller.dart @@ -2,15 +2,51 @@ import 'package:get/get.dart'; import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/controller/project_controller.dart'; +import 'package:marco/model/dashboard/project_progress_model.dart'; class DashboardController extends GetxController { - // Observables - final RxList> roleWiseData = >[].obs; - final RxBool isLoading = false.obs; - final RxString selectedRange = '15D'.obs; - final RxBool isChartView = true.obs; + // ========================= + // Attendance overview + // ========================= + final RxList> roleWiseData = + >[].obs; + final RxString attendanceSelectedRange = '15D'.obs; + final RxBool attendanceIsChartView = true.obs; + final RxBool isAttendanceLoading = false.obs; - // Inject the ProjectController + // ========================= + // 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']; + + // Inject ProjectController final ProjectController projectController = Get.find(); @override @@ -20,88 +56,208 @@ class DashboardController extends GetxController { logSafe( 'DashboardController initialized with project ID: ${projectController.selectedProjectId.value}', level: LogLevel.info, - ); - if (projectController.selectedProjectId.value.isNotEmpty) { - fetchRoleWiseAttendance(); - } + fetchAllDashboardData(); // React to project change ever(projectController.selectedProjectId, (id) { - if (id.isNotEmpty) { - logSafe('Project changed to $id, fetching attendance', level: LogLevel.info, ); - fetchRoleWiseAttendance(); - } + fetchAllDashboardData(); }); - // React to range change - ever(selectedRange, (_) { - fetchRoleWiseAttendance(); - }); + // React to range changes + ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance()); + ever(projectSelectedRange, (_) => fetchProjectProgress()); } - int get rangeDays => _getDaysFromRange(selectedRange.value); - + /// ========================= + /// Helper Methods + /// ========================= int _getDaysFromRange(String range) { switch (range) { + case '7D': + return 7; case '15D': return 15; case '30D': return 30; - case '7D': + case '3M': + return 90; + case '6M': + return 180; default: return 7; } } - void updateRange(String range) { - selectedRange.value = range; - logSafe('Selected range updated to $range', level: LogLevel.debug); + int getAttendanceDays() => _getDaysFromRange(attendanceSelectedRange.value); + int getProjectDays() => _getDaysFromRange(projectSelectedRange.value); + + void updateAttendanceRange(String range) { + attendanceSelectedRange.value = range; + logSafe('Attendance range updated to $range', level: LogLevel.debug); } - void toggleChartView(bool isChart) { - isChartView.value = isChart; - logSafe('Chart view toggled to: $isChart', level: LogLevel.debug); + void updateProjectRange(String range) { + projectSelectedRange.value = range; + logSafe('Project range updated to $range', level: LogLevel.debug); } + void toggleAttendanceChartView(bool isChart) { + attendanceIsChartView.value = isChart; + logSafe('Attendance chart view toggled to: $isChart', + level: LogLevel.debug); + } + + void toggleProjectChartView(bool isChart) { + projectIsChartView.value = isChart; + logSafe('Project chart view toggled to: $isChart', level: LogLevel.debug); + } + + /// ========================= + /// Manual refresh + /// ========================= Future refreshDashboard() async { logSafe('Manual dashboard refresh triggered.', level: LogLevel.debug); - await fetchRoleWiseAttendance(); + await fetchAllDashboardData(); } - 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 API call.', level: LogLevel.warning); + 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 { - isLoading.value = true; + isAttendanceLoading.value = true; final List? response = - await ApiService.getDashboardAttendanceOverview(projectId, rangeDays); + await ApiService.getDashboardAttendanceOverview( + projectId, getAttendanceDays()); if (response != null) { roleWiseData.value = response.map((e) => Map.from(e)).toList(); - logSafe('Attendance overview fetched successfully.', level: LogLevel.info); + logSafe('Attendance overview fetched successfully.', + level: LogLevel.info); } else { roleWiseData.clear(); - logSafe('Failed to fetch attendance overview: response is null.', level: LogLevel.error); + logSafe('Failed to fetch attendance overview: response is null.', + level: LogLevel.error); } } 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 { - isLoading.value = false; + isAttendanceLoading.value = false; + } + } + + Future fetchProjectProgress() async { + final String projectId = projectController.selectedProjectId.value; + + if (projectId.isEmpty) return; + + try { + isProjectLoading.value = true; + + final response = await ApiService.getProjectProgress( + projectId: projectId, + days: getProjectDays(), + ); + + if (response != null && response.success) { + projectChartData.value = + response.data.map((d) => ChartTaskData.fromProjectData(d)).toList(); + logSafe('Project progress data mapped for chart', level: LogLevel.info); + } else { + projectChartData.clear(); + logSafe('Failed to fetch project progress', level: LogLevel.error); + } + } catch (e, st) { + projectChartData.clear(); + 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/controller/dynamicMenu/dynamic_menu_controller.dart b/lib/controller/dynamicMenu/dynamic_menu_controller.dart index bbd9738..e7fb382 100644 --- a/lib/controller/dynamicMenu/dynamic_menu_controller.dart +++ b/lib/controller/dynamicMenu/dynamic_menu_controller.dart @@ -20,7 +20,7 @@ class DynamicMenuController extends GetxController { /// Auto refresh every 5 minutes (adjust as needed) _autoRefreshTimer = Timer.periodic( - const Duration(minutes: 1), + const Duration(minutes: 15), (_) => fetchMenu(), ); } diff --git a/lib/controller/dashboard/add_employee_controller.dart b/lib/controller/employee/add_employee_controller.dart similarity index 100% rename from lib/controller/dashboard/add_employee_controller.dart rename to lib/controller/employee/add_employee_controller.dart diff --git a/lib/controller/dashboard/employees_screen_controller.dart b/lib/controller/employee/employees_screen_controller.dart similarity index 100% rename from lib/controller/dashboard/employees_screen_controller.dart rename to lib/controller/employee/employees_screen_controller.dart diff --git a/lib/controller/dashboard/daily_task_controller.dart b/lib/controller/task_planning/daily_task_controller.dart similarity index 100% rename from lib/controller/dashboard/daily_task_controller.dart rename to lib/controller/task_planning/daily_task_controller.dart diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 252ccdc..9bf8e59 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -4,6 +4,11 @@ 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 e1648a4..4c64820 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -8,7 +8,9 @@ import 'package:marco/helpers/services/auth_service.dart'; 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 { @@ -625,6 +694,49 @@ class ApiService { : null); } + /// Fetch Project Progress + static Future getProjectProgress({ + required String projectId, + required int days, + DateTime? fromDate, // make optional + }) async { + const endpoint = ApiEndpoints.getDashboardProjectProgress; + + // Use today's date if fromDate is not provided + final actualFromDate = fromDate ?? DateTime.now(); + + final queryParams = { + "projectId": projectId, + "days": days.toString(), + "FromDate": + DateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS").format(actualFromDate), + }; + + try { + final response = await _getRequest(endpoint, queryParams: queryParams); + + if (response == null) { + logSafe( + "Project Progress request failed: null response", + level: LogLevel.error, + ); + return null; + } + + final parsed = + _parseResponseForAllData(response, label: "ProjectProgress"); + if (parsed != null) { + logSafe("✅ Project progress fetched successfully"); + return ProjectResponse.fromJson(parsed); + } + } catch (e, stack) { + logSafe("Exception during getProjectProgress: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + /// Directory calling the API static Future deleteBucket(String id) async { diff --git a/lib/helpers/services/notification_action_handler.dart b/lib/helpers/services/notification_action_handler.dart index 209b724..f044136 100644 --- a/lib/helpers/services/notification_action_handler.dart +++ b/lib/helpers/services/notification_action_handler.dart @@ -1,8 +1,8 @@ import 'package:get/get.dart'; import 'package:logger/logger.dart'; -import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; -import 'package:marco/controller/dashboard/daily_task_controller.dart'; +import 'package:marco/controller/attendance/attendance_screen_controller.dart'; +import 'package:marco/controller/task_planning/daily_task_controller.dart'; import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart'; import 'package:marco/controller/expense/expense_screen_controller.dart'; import 'package:marco/controller/expense/expense_detail_controller.dart'; diff --git a/lib/helpers/widgets/avatar.dart b/lib/helpers/widgets/avatar.dart index 82f1dbf..fdb32de 100644 --- a/lib/helpers/widgets/avatar.dart +++ b/lib/helpers/widgets/avatar.dart @@ -30,8 +30,9 @@ class Avatar extends StatelessWidget { paddingAll: 0, color: bgColor, child: Center( - child: MyText.labelSmall( + child: MyText( initials, + fontSize: size * 0.45, // 👈 scales with avatar size fontWeight: 600, color: textColor, ), diff --git a/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart b/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart new file mode 100644 index 0000000..9cf0a75 --- /dev/null +++ b/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart @@ -0,0 +1,469 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:marco/controller/dashboard/dashboard_controller.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; + +class AttendanceDashboardChart extends StatelessWidget { + AttendanceDashboardChart({Key? key}) : super(key: key); + + final DashboardController _controller = Get.find(); + + static const List _flatColors = [ + Color(0xFFE57373), // Red 300 + Color(0xFF64B5F6), // Blue 300 + Color(0xFF81C784), // Green 300 + Color(0xFFFFB74D), // Orange 300 + Color(0xFFBA68C8), // Purple 300 + Color(0xFFFF8A65), // Deep Orange 300 + Color(0xFF4DB6AC), // Teal 300 + Color(0xFFA1887F), // Brown 400 + Color(0xFFDCE775), // Lime 300 + Color(0xFF9575CD), // Deep Purple 300 + Color(0xFF7986CB), // Indigo 300 + Color(0xFFAED581), // Light Green 300 + Color(0xFFFF7043), // Deep Orange 400 + Color(0xFF4FC3F7), // Light Blue 300 + Color(0xFFFFD54F), // Amber 300 + Color(0xFF90A4AE), // Blue Grey 300 + Color(0xFFE573BB), // Pink 300 + Color(0xFF81D4FA), // Light Blue 200 + Color(0xFFBCAAA4), // Brown 300 + Color(0xFFA5D6A7), // Green 300 + Color(0xFFCE93D8), // Purple 200 + Color(0xFFFF8A65), // Deep Orange 300 (repeat to fill) + Color(0xFF80CBC4), // Teal 200 + Color(0xFFFFF176), // Yellow 300 + Color(0xFF90CAF9), // Blue 200 + Color(0xFFE0E0E0), // Grey 300 + Color(0xFFF48FB1), // Pink 200 + Color(0xFFA1887F), // Brown 400 (repeat) + Color(0xFFB0BEC5), // Blue Grey 200 + Color(0xFF81C784), // Green 300 (repeat) + Color(0xFFFFB74D), // Orange 300 (repeat) + Color(0xFF64B5F6), // Blue 300 (repeat) + ]; + + static final Map _roleColorMap = {}; + + Color _getRoleColor(String role) { + return _roleColorMap.putIfAbsent( + role, + () => _flatColors[_roleColorMap.length % _flatColors.length], + ); + } + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + + return Obx(() { + final isChartView = _controller.attendanceIsChartView.value; + final selectedRange = _controller.attendanceSelectedRange.value; + final isLoading = _controller.isAttendanceLoading.value; + + final filteredData = _getFilteredData(); + if (isLoading) { + return SkeletonLoaders.buildLoadingSkeleton(); + } + + return Container( + decoration: _containerDecoration, + padding: EdgeInsets.symmetric( + vertical: 16, + horizontal: screenWidth < 600 ? 8 : 20, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Header( + selectedRange: selectedRange, + isChartView: isChartView, + screenWidth: screenWidth, + onToggleChanged: (isChart) => + _controller.attendanceIsChartView.value = isChart, + onRangeChanged: _controller.updateAttendanceRange, + ), + const SizedBox(height: 12), + Expanded( + child: filteredData.isEmpty + ? _NoDataMessage() + : isChartView + ? _AttendanceChart( + data: filteredData, getRoleColor: _getRoleColor) + : _AttendanceTable( + data: filteredData, + getRoleColor: _getRoleColor, + screenWidth: screenWidth), + ), + ], + ), + ); + }); + } + + BoxDecoration get _containerDecoration => BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.05), + blurRadius: 6, + spreadRadius: 1, + offset: const Offset(0, 2), + ), + ], + ); + + List> _getFilteredData() { + final now = DateTime.now(); + final daysBack = _controller.getAttendanceDays(); + return _controller.roleWiseData.where((entry) { + final date = DateTime.parse(entry['date'] as String); + return date.isAfter(now.subtract(Duration(days: daysBack))) && + !date.isAfter(now); + }).toList(); + } +} + +// Header +class _Header extends StatelessWidget { + const _Header({ + Key? key, + required this.selectedRange, + required this.isChartView, + required this.screenWidth, + required this.onToggleChanged, + required this.onRangeChanged, + }) : super(key: key); + + final String selectedRange; + final bool isChartView; + final double screenWidth; + final ValueChanged onToggleChanged; + final ValueChanged onRangeChanged; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium('Attendance Overview', fontWeight: 700), + const SizedBox(height: 2), + MyText.bodySmall('Role-wise present count', + color: Colors.grey), + ], + ), + ), + ToggleButtons( + borderRadius: BorderRadius.circular(6), + borderColor: Colors.grey, + fillColor: Colors.blueAccent.withOpacity(0.15), + selectedBorderColor: Colors.blueAccent, + selectedColor: Colors.blueAccent, + color: Colors.grey, + constraints: BoxConstraints( + minHeight: 30, + minWidth: screenWidth < 400 ? 28 : 36, + ), + isSelected: [isChartView, !isChartView], + onPressed: (index) => onToggleChanged(index == 0), + children: const [ + Icon(Icons.bar_chart_rounded, size: 15), + Icon(Icons.table_chart, size: 15), + ], + ), + ], + ), + const SizedBox(height: 8), + Row( + children: ["7D", "15D", "30D"] + .map( + (label) => Padding( + padding: const EdgeInsets.only(right: 4), + child: ChoiceChip( + label: Text(label, style: const TextStyle(fontSize: 12)), + padding: + const EdgeInsets.symmetric(horizontal: 5, vertical: 0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + selected: selectedRange == label, + onSelected: (_) => onRangeChanged(label), + selectedColor: Colors.blueAccent.withOpacity(0.15), + backgroundColor: Colors.grey.shade200, + labelStyle: TextStyle( + color: selectedRange == label + ? Colors.blueAccent + : Colors.black87, + fontWeight: selectedRange == label + ? FontWeight.w600 + : FontWeight.normal, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + side: BorderSide( + color: selectedRange == label + ? Colors.blueAccent + : Colors.grey.shade300, + ), + ), + ), + ), + ) + .toList(), + ), + ], + ); + } +} + +// No Data +class _NoDataMessage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return SizedBox( + height: 180, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.info_outline, color: Colors.grey.shade400, size: 48), + const SizedBox(height: 10), + MyText.bodyMedium( + 'No attendance data available for this range.', + textAlign: TextAlign.center, + color: Colors.grey.shade500, + ), + ], + ), + ), + ); + } +} + +// Chart +class _AttendanceChart extends StatelessWidget { + const _AttendanceChart({ + Key? key, + required this.data, + required this.getRoleColor, + }) : super(key: key); + + final List> data; + final Color Function(String role) getRoleColor; + + @override + Widget build(BuildContext context) { + final dateFormat = DateFormat('d MMMM'); + final uniqueDates = data + .map((e) => DateTime.parse(e['date'] as String)) + .toSet() + .toList() + ..sort(); + final filteredDates = uniqueDates.map(dateFormat.format).toList(); + + final filteredRoles = data.map((e) => e['role'] as String).toSet().toList(); + + final allZero = filteredRoles.every((role) { + return data + .where((entry) => entry['role'] == role) + .every((entry) => (entry['present'] ?? 0) == 0); + }); + + if (allZero) { + return Container( + height: 600, + decoration: BoxDecoration( + color: Colors.blueGrey.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: const Center( + child: Text( + 'No attendance data for the selected range.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, color: Colors.grey), + ), + ), + ); + } + + final formattedMap = { + for (var e in data) + '${e['role']}_${dateFormat.format(DateTime.parse(e['date'] as String))}': + e['present'], + }; + + final rolesWithData = filteredRoles.where((role) { + return data + .any((entry) => entry['role'] == role && (entry['present'] ?? 0) > 0); + }).toList(); + + return Container( + height: 600, + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.blueGrey.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: SfCartesianChart( + tooltipBehavior: TooltipBehavior(enable: true, shared: true), + legend: Legend(isVisible: true, position: LegendPosition.bottom), + primaryXAxis: CategoryAxis(labelRotation: 45), + primaryYAxis: NumericAxis(minimum: 0, interval: 1), + series: rolesWithData.map((role) { + final seriesData = filteredDates + .map((date) { + final key = '${role}_$date'; + return {'date': date, 'present': formattedMap[key] ?? 0}; + }) + .where((d) => (d['present'] ?? 0) > 0) + .toList(); // ✅ remove 0 bars + + return StackedColumnSeries, String>( + dataSource: seriesData, + xValueMapper: (d, _) => d['date'], + yValueMapper: (d, _) => d['present'], + name: role, + color: getRoleColor(role), + dataLabelSettings: DataLabelSettings( + isVisible: true, + builder: (dynamic data, _, __, ___, ____) { + return (data['present'] ?? 0) > 0 + ? Text( + NumberFormat.decimalPattern().format(data['present']), + style: const TextStyle(fontSize: 11), + ) + : const SizedBox.shrink(); + }, + ), + ); + }).toList(), + ), + ); + } +} + +// Table +class _AttendanceTable extends StatelessWidget { + const _AttendanceTable({ + Key? key, + required this.data, + required this.getRoleColor, + required this.screenWidth, + }) : super(key: key); + + final List> data; + final Color Function(String role) getRoleColor; + final double screenWidth; + + @override + Widget build(BuildContext context) { + final dateFormat = DateFormat('d MMMM'); + final uniqueDates = data + .map((e) => DateTime.parse(e['date'] as String)) + .toSet() + .toList() + ..sort(); + final filteredDates = uniqueDates.map(dateFormat.format).toList(); + + final filteredRoles = data.map((e) => e['role'] as String).toSet().toList(); + + final allZero = filteredRoles.every((role) { + return data + .where((entry) => entry['role'] == role) + .every((entry) => (entry['present'] ?? 0) == 0); + }); + + if (allZero) { + return Container( + height: 300, + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Text( + 'No attendance data for the selected range.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, color: Colors.grey), + ), + ), + ); + } + + final formattedMap = { + for (var e in data) + '${e['role']}_${dateFormat.format(DateTime.parse(e['date'] as String))}': + e['present'], + }; + + return Container( + height: 300, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + color: Colors.grey.shade50, + ), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: DataTable( + columnSpacing: screenWidth < 600 ? 20 : 36, + headingRowHeight: 44, + headingRowColor: + MaterialStateProperty.all(Colors.blueAccent.withOpacity(0.08)), + headingTextStyle: const TextStyle( + fontWeight: FontWeight.bold, color: Colors.black87), + columns: [ + const DataColumn(label: Text('Role')), + ...filteredDates.map((d) => DataColumn(label: Text(d))), + ], + rows: filteredRoles.map((role) { + return DataRow( + cells: [ + DataCell(_RolePill(role: role, color: getRoleColor(role))), + ...filteredDates.map((date) { + final key = '${role}_$date'; + return DataCell( + Text( + NumberFormat.decimalPattern() + .format(formattedMap[key] ?? 0), + style: const TextStyle(fontSize: 13), + ), + ); + }), + ], + ); + }).toList(), + ), + ), + ); + } +} + +class _RolePill extends StatelessWidget { + const _RolePill({Key? key, required this.role, required this.color}) + : super(key: key); + + final String role; + final Color color; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withOpacity(0.15), + borderRadius: BorderRadius.circular(6), + ), + child: MyText.labelSmall(role, fontWeight: 500), + ); + } +} 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..ac21276 --- /dev/null +++ b/lib/helpers/widgets/dashbaord/dashboard_overview_widgets.dart @@ -0,0 +1,277 @@ +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 +import 'package:intl/intl.dart'; + +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, + ); + static final NumberFormat _commaFormatter = NumberFormat.decimalPattern(); + + /// 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(_commaFormatter.format(total), + style: _infoNumberTextStyle), + MyText(_commaFormatter.format(inToday), + 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(_commaFormatter.format(value), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + )), + 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/helpers/widgets/dashbaord/project_progress_chart.dart b/lib/helpers/widgets/dashbaord/project_progress_chart.dart new file mode 100644 index 0000000..3162f11 --- /dev/null +++ b/lib/helpers/widgets/dashbaord/project_progress_chart.dart @@ -0,0 +1,366 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:marco/model/dashboard/project_progress_model.dart'; +import 'package:marco/controller/dashboard/dashboard_controller.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; + +class ProjectProgressChart extends StatelessWidget { + final List data; + final DashboardController controller = Get.find(); + + ProjectProgressChart({super.key, required this.data}); + + // ================= Flat Colors ================= + static const List _flatColors = [ + Color(0xFFE57373), + Color(0xFF64B5F6), + Color(0xFF81C784), + Color(0xFFFFB74D), + Color(0xFFBA68C8), + Color(0xFFFF8A65), + Color(0xFF4DB6AC), + Color(0xFFA1887F), + Color(0xFFDCE775), + Color(0xFF9575CD), + Color(0xFF7986CB), + Color(0xFFAED581), + Color(0xFFFF7043), + Color(0xFF4FC3F7), + Color(0xFFFFD54F), + Color(0xFF90A4AE), + Color(0xFFE573BB), + Color(0xFF81D4FA), + Color(0xFFBCAAA4), + Color(0xFFA5D6A7), + Color(0xFFCE93D8), + Color(0xFFFF8A65), + Color(0xFF80CBC4), + Color(0xFFFFF176), + Color(0xFF90CAF9), + Color(0xFFE0E0E0), + Color(0xFFF48FB1), + Color(0xFFA1887F), + Color(0xFFB0BEC5), + Color(0xFF81C784), + Color(0xFFFFB74D), + Color(0xFF64B5F6), + ]; + static final NumberFormat _commaFormatter = NumberFormat.decimalPattern(); + + static final Map _taskColorMap = {}; + + Color _getTaskColor(String taskName) { + return _taskColorMap.putIfAbsent( + taskName, + () => _flatColors[_taskColorMap.length % _flatColors.length], + ); + } + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + + return Obx(() { + final isChartView = controller.projectIsChartView.value; + final selectedRange = controller.projectSelectedRange.value; + final isLoading = controller.isProjectLoading.value; + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.04), + blurRadius: 6, + spreadRadius: 1, + offset: Offset(0, 2), + ), + ], + ), + padding: EdgeInsets.symmetric( + vertical: 16, + horizontal: screenWidth < 600 ? 8 : 24, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(selectedRange, isChartView, screenWidth), + const SizedBox(height: 14), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) => AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: isLoading + ? SkeletonLoaders.buildLoadingSkeleton() + : data.isEmpty + ? _buildNoDataMessage() + : isChartView + ? _buildChart(constraints.maxHeight) + : _buildTable(constraints.maxHeight, screenWidth), + ), + ), + ), + ], + ), + ); + }); + } + + Widget _buildHeader( + String selectedRange, bool isChartView, double screenWidth) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium('Project Progress', fontWeight: 700), + MyText.bodySmall('Planned vs Completed', + color: Colors.grey.shade700), + ], + ), + ), + ToggleButtons( + borderRadius: BorderRadius.circular(6), + borderColor: Colors.grey, + fillColor: Colors.blueAccent.withOpacity(0.15), + selectedBorderColor: Colors.blueAccent, + selectedColor: Colors.blueAccent, + color: Colors.grey, + constraints: BoxConstraints( + minHeight: 30, + minWidth: (screenWidth < 400 ? 28 : 36), + ), + isSelected: [isChartView, !isChartView], + onPressed: (index) { + controller.projectIsChartView.value = index == 0; + }, + children: const [ + Icon(Icons.bar_chart_rounded, size: 15), + Icon(Icons.table_chart, size: 15), + ], + ), + ], + ), + const SizedBox(height: 6), + Row( + children: [ + _buildRangeButton("7D", selectedRange), + _buildRangeButton("15D", selectedRange), + _buildRangeButton("30D", selectedRange), + _buildRangeButton("3M", selectedRange), + _buildRangeButton("6M", selectedRange), + ], + ), + ], + ); + } + + Widget _buildRangeButton(String label, String selectedRange) { + return Padding( + padding: const EdgeInsets.only(right: 4.0), + child: ChoiceChip( + label: Text(label, style: const TextStyle(fontSize: 12)), + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + selected: selectedRange == label, + onSelected: (_) => controller.updateProjectRange(label), + selectedColor: Colors.blueAccent.withOpacity(0.15), + backgroundColor: Colors.grey.shade200, + labelStyle: TextStyle( + color: selectedRange == label ? Colors.blueAccent : Colors.black87, + fontWeight: + selectedRange == label ? FontWeight.w600 : FontWeight.normal, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + side: BorderSide( + color: selectedRange == label + ? Colors.blueAccent + : Colors.grey.shade300, + ), + ), + ), + ); + } + + Widget _buildChart(double height) { + final nonZeroData = + data.where((d) => d.planned != 0 || d.completed != 0).toList(); + + if (nonZeroData.isEmpty) { + return _buildNoDataContainer(height); + } + + return Container( + height: height > 280 ? 280 : height, + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.blueGrey.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: SfCartesianChart( + tooltipBehavior: TooltipBehavior(enable: true), + legend: Legend(isVisible: true, position: LegendPosition.bottom), + // ✅ Use CategoryAxis so only nonZeroData dates show up + primaryXAxis: CategoryAxis( + majorGridLines: const MajorGridLines(width: 0), + axisLine: const AxisLine(width: 0), + labelRotation: 0, + ), + primaryYAxis: NumericAxis( + labelFormat: '{value}', + axisLine: const AxisLine(width: 0), + majorTickLines: const MajorTickLines(size: 0), + ), + series: [ + ColumnSeries( + name: 'Planned', + dataSource: nonZeroData, + xValueMapper: (d, _) => DateFormat('MMM d').format(d.date), + yValueMapper: (d, _) => d.planned, + color: _getTaskColor('Planned'), + dataLabelSettings: DataLabelSettings( + isVisible: true, + builder: (data, point, series, pointIndex, seriesIndex) { + final value = seriesIndex == 0 + ? (data as ChartTaskData).planned + : (data as ChartTaskData).completed; + return Text( + _commaFormatter.format(value), + style: const TextStyle(fontSize: 11), + ); + }, + ), + ), + ColumnSeries( + name: 'Completed', + dataSource: nonZeroData, + xValueMapper: (d, _) => DateFormat('MMM d').format(d.date), + yValueMapper: (d, _) => d.completed, + color: _getTaskColor('Completed'), + dataLabelSettings: DataLabelSettings( + isVisible: true, + builder: (data, point, series, pointIndex, seriesIndex) { + final value = seriesIndex == 0 + ? (data as ChartTaskData).planned + : (data as ChartTaskData).completed; + return Text( + _commaFormatter.format(value), + style: const TextStyle(fontSize: 11), + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildTable(double maxHeight, double screenWidth) { + final containerHeight = maxHeight > 300 ? 300.0 : maxHeight; + final nonZeroData = + data.where((d) => d.planned != 0 || d.completed != 0).toList(); + + if (nonZeroData.isEmpty) { + return _buildNoDataContainer(containerHeight); + } + + return Container( + height: containerHeight, + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + color: Colors.grey.shade50, + ), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: constraints.maxWidth), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: DataTable( + columnSpacing: screenWidth < 600 ? 16 : 36, + headingRowHeight: 44, + headingRowColor: MaterialStateProperty.all( + Colors.blueAccent.withOpacity(0.08)), + headingTextStyle: const TextStyle( + fontWeight: FontWeight.bold, color: Colors.black87), + columns: const [ + DataColumn(label: Text('Date')), + DataColumn(label: Text('Planned')), + DataColumn(label: Text('Completed')), + ], + rows: nonZeroData.map((task) { + return DataRow( + cells: [ + DataCell(Text(DateFormat('d MMM').format(task.date))), + DataCell(Text( + '${task.planned}', + style: TextStyle(color: _getTaskColor('Planned')), + )), + DataCell(Text( + '${task.completed}', + style: TextStyle(color: _getTaskColor('Completed')), + )), + ], + ); + }).toList(), + ), + ), + ), + ); + }, + ), + ); + } + + Widget _buildNoDataContainer(double height) { + return Container( + height: height > 280 ? 280 : height, + decoration: BoxDecoration( + color: Colors.blueGrey.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: const Center( + child: Text( + 'No project progress data for the selected range.', + style: TextStyle(fontSize: 14, color: Colors.grey), + textAlign: TextAlign.center, + ), + ), + ); + } + + Widget _buildNoDataMessage() { + return SizedBox( + height: 180, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.info_outline, color: Colors.grey.shade400, size: 54), + const SizedBox(height: 10), + MyText.bodyMedium( + 'No project progress data available for the selected range.', + textAlign: TextAlign.center, + color: Colors.grey.shade500, + ), + ], + ), + ), + ); + } +} diff --git a/lib/helpers/widgets/expense_detail_helpers.dart b/lib/helpers/widgets/expense/expense_detail_helpers.dart similarity index 100% rename from lib/helpers/widgets/expense_detail_helpers.dart rename to lib/helpers/widgets/expense/expense_detail_helpers.dart diff --git a/lib/helpers/widgets/expense_main_components.dart b/lib/helpers/widgets/expense/expense_main_components.dart similarity index 100% rename from lib/helpers/widgets/expense_main_components.dart rename to lib/helpers/widgets/expense/expense_main_components.dart diff --git a/lib/helpers/widgets/image_viewer_dialog.dart b/lib/helpers/widgets/image_viewer_dialog.dart index 591c56b..fadfdcc 100644 --- a/lib/helpers/widgets/image_viewer_dialog.dart +++ b/lib/helpers/widgets/image_viewer_dialog.dart @@ -43,7 +43,7 @@ class _ImageViewerDialogState extends State { color: Colors.white, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 12, offset: const Offset(0, 4), ), @@ -92,8 +92,7 @@ class _ImageViewerDialogState extends State { }, errorBuilder: (context, error, stackTrace) => const Center( - child: Icon(Icons.broken_image, - size: 48, color: Colors.grey), + child: Icon(Icons.broken_image, size: 48, color: Colors.grey), ), ), ); diff --git a/lib/helpers/widgets/my_confirmation_dialog.dart b/lib/helpers/widgets/my_confirmation_dialog.dart index f1155d6..c1c15d4 100644 --- a/lib/helpers/widgets/my_confirmation_dialog.dart +++ b/lib/helpers/widgets/my_confirmation_dialog.dart @@ -84,7 +84,7 @@ class _ContentView extends StatelessWidget { MyText.bodySmall( message, textAlign: TextAlign.center, - color: theme.colorScheme.onSurface.withOpacity(0.7), + color: theme.colorScheme.onSurface.withValues(alpha: 0.7), ), const SizedBox(height: 24), Row( diff --git a/lib/model/attendance/attendence_action_button.dart b/lib/model/attendance/attendence_action_button.dart index 7d61c5d..40c398e 100644 --- a/lib/model/attendance/attendence_action_button.dart +++ b/lib/model/attendance/attendence_action_button.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; -import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; +import 'package:marco/controller/attendance/attendance_screen_controller.dart'; import 'package:marco/helpers/utils/attendance_actions.dart'; import 'package:marco/controller/project_controller.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; diff --git a/lib/model/attendance/attendence_filter_sheet.dart b/lib/model/attendance/attendence_filter_sheet.dart index c275eb1..bbf7b2b 100644 --- a/lib/model/attendance/attendence_filter_sheet.dart +++ b/lib/model/attendance/attendence_filter_sheet.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:marco/controller/permission_controller.dart'; -import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; +import 'package:marco/controller/attendance/attendance_screen_controller.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; diff --git a/lib/model/dailyTaskPlanning/daily_progress_report_filter.dart b/lib/model/dailyTaskPlanning/daily_progress_report_filter.dart index 586a0d2..7a6744b 100644 --- a/lib/model/dailyTaskPlanning/daily_progress_report_filter.dart +++ b/lib/model/dailyTaskPlanning/daily_progress_report_filter.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:marco/controller/dashboard/daily_task_controller.dart'; +import 'package:marco/controller/task_planning/daily_task_controller.dart'; import 'package:marco/controller/permission_controller.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; import 'package:marco/helpers/widgets/my_text.dart'; 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/model/dashboard/project_progress_model.dart b/lib/model/dashboard/project_progress_model.dart new file mode 100644 index 0000000..20f47c1 --- /dev/null +++ b/lib/model/dashboard/project_progress_model.dart @@ -0,0 +1,104 @@ +import 'package:intl/intl.dart'; + +class ProjectResponse { + final bool success; + final String message; + final List data; + final dynamic errors; + final int statusCode; + final DateTime timestamp; + + ProjectResponse({ + required this.success, + required this.message, + required this.data, + this.errors, + required this.statusCode, + required this.timestamp, + }); + + factory ProjectResponse.fromJson(Map json) { + return ProjectResponse( + success: json['success'] ?? false, + message: json['message'] ?? '', + data: (json['data'] as List?) + ?.map((e) => ProjectData.fromJson(e)) + .toList() ?? + [], + errors: json['errors'], + statusCode: json['statusCode'] ?? 0, + timestamp: DateTime.parse(json['timestamp']), + ); + } + + Map toJson() { + return { + 'success': success, + 'message': message, + 'data': data.map((e) => e.toJson()).toList(), + 'errors': errors, + 'statusCode': statusCode, + 'timestamp': timestamp.toIso8601String(), + }; + } +} + +class ProjectData { + final String projectId; + final String projectName; + final int plannedTask; + final int completedTask; + final DateTime date; + + ProjectData({ + required this.projectId, + required this.projectName, + required this.plannedTask, + required this.completedTask, + required this.date, + }); + + factory ProjectData.fromJson(Map json) { + return ProjectData( + projectId: json['projectId'] ?? '', + projectName: json['projectName'] ?? '', + plannedTask: json['plannedTask'] ?? 0, + completedTask: json['completedTask'] ?? 0, + date: DateTime.parse(json['date']), + ); + } + + Map toJson() { + return { + 'projectId': projectId, + 'projectName': projectName, + 'plannedTask': plannedTask, + 'completedTask': completedTask, + 'date': date.toIso8601String(), + }; + } +} + +/// Chart-friendly model +class ChartTaskData { + final DateTime date; // ✅ actual date for chart + final String dateLabel; // optional: for display + final int planned; + final int completed; + + ChartTaskData({ + required this.date, + required this.dateLabel, + required this.planned, + required this.completed, + }); + + factory ChartTaskData.fromProjectData(ProjectData data) { + return ChartTaskData( + date: data.date, + dateLabel: DateFormat('dd-MM').format(data.date), + planned: data.plannedTask, + completed: data.completedTask, + ); + } +} diff --git a/lib/model/employees/add_employee_bottom_sheet.dart b/lib/model/employees/add_employee_bottom_sheet.dart index e4fd3b3..993bb3d 100644 --- a/lib/model/employees/add_employee_bottom_sheet.dart +++ b/lib/model/employees/add_employee_bottom_sheet.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:marco/controller/dashboard/add_employee_controller.dart'; -import 'package:marco/controller/dashboard/employees_screen_controller.dart'; +import 'package:marco/controller/employee/add_employee_controller.dart'; +import 'package:marco/controller/employee/employees_screen_controller.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; diff --git a/lib/model/employees/employee_detail_bottom_sheet.dart b/lib/model/employees/employee_detail_bottom_sheet.dart index 3a0ef87..29e9f24 100644 --- a/lib/model/employees/employee_detail_bottom_sheet.dart +++ b/lib/model/employees/employee_detail_bottom_sheet.dart @@ -4,7 +4,7 @@ import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:intl/intl.dart'; -import 'package:marco/controller/dashboard/employees_screen_controller.dart'; +import 'package:marco/controller/employee/employees_screen_controller.dart'; import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; class EmployeeDetailBottomSheet extends StatefulWidget { diff --git a/lib/model/employees/employees_screen_filter_sheet.dart b/lib/model/employees/employees_screen_filter_sheet.dart index d47957a..24f672c 100644 --- a/lib/model/employees/employees_screen_filter_sheet.dart +++ b/lib/model/employees/employees_screen_filter_sheet.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:marco/controller/permission_controller.dart'; -import 'package:marco/controller/dashboard/employees_screen_controller.dart'; +import 'package:marco/controller/employee/employees_screen_controller.dart'; import 'package:marco/helpers/widgets/my_text.dart'; class EmployeesScreenFilterSheet extends StatelessWidget { diff --git a/lib/routes.dart b/lib/routes.dart index bda105b..3be20d3 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -9,7 +9,7 @@ import 'package:marco/view/error_pages/coming_soon_screen.dart'; import 'package:marco/view/error_pages/error_404_screen.dart'; import 'package:marco/view/error_pages/error_500_screen.dart'; import 'package:marco/view/dashboard/dashboard_screen.dart'; -import 'package:marco/view/dashboard/Attendence/attendance_screen.dart'; +import 'package:marco/view/Attendence/attendance_screen.dart'; import 'package:marco/view/taskPlanning/daily_task_planning.dart'; import 'package:marco/view/taskPlanning/daily_progress.dart'; import 'package:marco/view/employees/employees_screen.dart'; diff --git a/lib/view/dashboard/Attendence/attendance_logs_tab.dart b/lib/view/Attendence/attendance_logs_tab.dart similarity index 99% rename from lib/view/dashboard/Attendence/attendance_logs_tab.dart rename to lib/view/Attendence/attendance_logs_tab.dart index 2aeeb3d..53dc1c2 100644 --- a/lib/view/dashboard/Attendence/attendance_logs_tab.dart +++ b/lib/view/Attendence/attendance_logs_tab.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; +import 'package:marco/controller/attendance/attendance_screen_controller.dart'; import 'package:marco/helpers/utils/date_time_utils.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/my_card.dart'; diff --git a/lib/view/dashboard/Attendence/attendance_screen.dart b/lib/view/Attendence/attendance_screen.dart similarity index 96% rename from lib/view/dashboard/Attendence/attendance_screen.dart rename to lib/view/Attendence/attendance_screen.dart index afed054..116d27e 100644 --- a/lib/view/dashboard/Attendence/attendance_screen.dart +++ b/lib/view/Attendence/attendance_screen.dart @@ -6,13 +6,13 @@ import 'package:marco/helpers/widgets/my_flex.dart'; import 'package:marco/helpers/widgets/my_flex_item.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; -import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; +import 'package:marco/controller/attendance/attendance_screen_controller.dart'; import 'package:marco/controller/permission_controller.dart'; import 'package:marco/model/attendance/attendence_filter_sheet.dart'; import 'package:marco/controller/project_controller.dart'; -import 'package:marco/view/dashboard/Attendence/regularization_requests_tab.dart'; -import 'package:marco/view/dashboard/Attendence/attendance_logs_tab.dart'; -import 'package:marco/view/dashboard/Attendence/todays_attendance_tab.dart'; +import 'package:marco/view/Attendence/regularization_requests_tab.dart'; +import 'package:marco/view/Attendence/attendance_logs_tab.dart'; +import 'package:marco/view/Attendence/todays_attendance_tab.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; class AttendanceScreen extends StatefulWidget { const AttendanceScreen({super.key}); diff --git a/lib/view/dashboard/Attendence/attendence_log_screen.dart b/lib/view/Attendence/attendence_log_screen.dart similarity index 99% rename from lib/view/dashboard/Attendence/attendence_log_screen.dart rename to lib/view/Attendence/attendence_log_screen.dart index 0b586d1..05266ae 100644 --- a/lib/view/dashboard/Attendence/attendence_log_screen.dart +++ b/lib/view/Attendence/attendence_log_screen.dart @@ -12,7 +12,7 @@ import 'package:marco/helpers/widgets/my_flex_item.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/view/layouts/layout.dart'; -import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; +import 'package:marco/controller/attendance/attendance_screen_controller.dart'; import 'package:marco/controller/permission_controller.dart'; import 'package:intl/intl.dart'; import 'package:marco/model/attendance/attendence_action_button.dart'; diff --git a/lib/view/dashboard/Attendence/regularization_requests_tab.dart b/lib/view/Attendence/regularization_requests_tab.dart similarity index 98% rename from lib/view/dashboard/Attendence/regularization_requests_tab.dart rename to lib/view/Attendence/regularization_requests_tab.dart index 802075c..323dcbf 100644 --- a/lib/view/dashboard/Attendence/regularization_requests_tab.dart +++ b/lib/view/Attendence/regularization_requests_tab.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; -import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; +import 'package:marco/controller/attendance/attendance_screen_controller.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/my_card.dart'; import 'package:marco/helpers/widgets/my_container.dart'; diff --git a/lib/view/dashboard/Attendence/todays_attendance_tab.dart b/lib/view/Attendence/todays_attendance_tab.dart similarity index 98% rename from lib/view/dashboard/Attendence/todays_attendance_tab.dart rename to lib/view/Attendence/todays_attendance_tab.dart index 5697ade..d87ec76 100644 --- a/lib/view/dashboard/Attendence/todays_attendance_tab.dart +++ b/lib/view/Attendence/todays_attendance_tab.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; +import 'package:marco/controller/attendance/attendance_screen_controller.dart'; import 'package:marco/helpers/utils/date_time_utils.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/my_card.dart'; diff --git a/lib/view/dashboard/dashboard_chart.dart b/lib/view/dashboard/dashboard_chart.dart deleted file mode 100644 index edcd1ed..0000000 --- a/lib/view/dashboard/dashboard_chart.dart +++ /dev/null @@ -1,306 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:intl/intl.dart'; -import 'package:syncfusion_flutter_charts/charts.dart'; -import 'package:marco/controller/dashboard/dashboard_controller.dart'; -import 'package:marco/helpers/widgets/my_text.dart'; -import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; - -class AttendanceDashboardChart extends StatelessWidget { - final DashboardController controller = Get.find(); - - AttendanceDashboardChart({super.key}); - - List> get filteredData { - final now = DateTime.now(); - final daysBack = controller.rangeDays; - return controller.roleWiseData.where((entry) { - final date = DateTime.parse(entry['date']); - return date.isAfter(now.subtract(Duration(days: daysBack))) && - !date.isAfter(now); - }).toList(); - } - - List get filteredDateTimes { - final uniqueDates = filteredData - .map((e) => DateTime.parse(e['date'] as String)) - .toSet() - .toList() - ..sort(); - return uniqueDates; - } - - List get filteredDates => - filteredDateTimes.map((d) => DateFormat('d MMMM').format(d)).toList(); - - List get filteredRoles => - filteredData.map((e) => e['role'] as String).toSet().toList(); - - List get rolesWithData => filteredRoles.where((role) { - return filteredData.any( - (entry) => entry['role'] == role && (entry['present'] ?? 0) > 0); - }).toList(); - - final Map _roleColorMap = {}; - final List flatColors = [ - const Color(0xFFE57373), - const Color(0xFF64B5F6), - const Color(0xFF81C784), - const Color(0xFFFFB74D), - const Color(0xFFBA68C8), - const Color(0xFFFF8A65), - const Color(0xFF4DB6AC), - const Color(0xFFA1887F), - const Color(0xFFDCE775), - const Color(0xFF9575CD), - ]; - - Color _getRoleColor(String role) { - if (_roleColorMap.containsKey(role)) return _roleColorMap[role]!; - final index = _roleColorMap.length % flatColors.length; - final color = flatColors[index]; - _roleColorMap[role] = color; - return color; - } - - @override -Widget build(BuildContext context) { - return Obx(() { - final isChartView = controller.isChartView.value; - final selectedRange = controller.selectedRange.value; - final isLoading = controller.isLoading.value; - - return Container( - // flat white background - color: Colors.white, - padding: const EdgeInsets.all(10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeader(selectedRange, isChartView), - const SizedBox(height: 12), - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: isLoading - ? SkeletonLoaders.buildLoadingSkeleton() - : filteredData.isEmpty - ? _buildNoDataMessage() - : isChartView - ? _buildChart() - : _buildTable(), - ), - ], - ), - ); - }); -} - - Widget _buildHeader(String selectedRange, bool isChartView) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodyMedium('Attendance Overview', fontWeight: 600), - MyText.bodySmall('Role-wise present count', color: Colors.grey), - ], - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.shade300), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - PopupMenuButton( - padding: EdgeInsets.zero, - tooltip: 'Select Range', - onSelected: (value) => controller.selectedRange.value = value, - itemBuilder: (context) => const [ - PopupMenuItem(value: '7D', child: Text('Last 7 Days')), - PopupMenuItem(value: '15D', child: Text('Last 15 Days')), - PopupMenuItem(value: '30D', child: Text('Last 30 Days')), - ], - child: Row( - children: [ - const Icon(Icons.calendar_today_outlined, size: 18), - const SizedBox(width: 4), - MyText.labelSmall(selectedRange), - ], - ), - ), - const SizedBox(width: 8), - IconButton( - icon: Icon( - Icons.bar_chart_rounded, - size: 20, - color: isChartView ? Colors.blueAccent : Colors.grey, - ), - visualDensity: VisualDensity(horizontal: -4, vertical: -4), - constraints: const BoxConstraints(), - padding: EdgeInsets.zero, - onPressed: () => controller.isChartView.value = true, - tooltip: 'Chart View', - ), - IconButton( - icon: Icon( - Icons.table_chart, - size: 20, - color: !isChartView ? Colors.blueAccent : Colors.grey, - ), - visualDensity: VisualDensity(horizontal: -4, vertical: -4), - constraints: const BoxConstraints(), - padding: EdgeInsets.zero, - onPressed: () => controller.isChartView.value = false, - tooltip: 'Table View', - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildChart() { - final formattedDateMap = { - for (var e in filteredData) - '${e['role']}_${DateFormat('d MMMM').format(DateTime.parse(e['date']))}': - e['present'] - }; - - return SizedBox( - height: 360, - child: SfCartesianChart( - tooltipBehavior: TooltipBehavior( - enable: true, - shared: true, - activationMode: ActivationMode.singleTap, - tooltipPosition: TooltipPosition.pointer, - ), - legend: const Legend( - isVisible: true, - position: LegendPosition.bottom, - overflowMode: LegendItemOverflowMode.wrap, - ), - primaryXAxis: CategoryAxis( - labelRotation: 45, - majorGridLines: const MajorGridLines(width: 0), - ), - primaryYAxis: NumericAxis( - minimum: 0, - interval: 1, - majorGridLines: const MajorGridLines(width: 0), - ), - series: rolesWithData.map((role) { - final data = filteredDates.map((formattedDate) { - final key = '${role}_$formattedDate'; - return { - 'date': formattedDate, - 'present': formattedDateMap[key] ?? 0 - }; - }).toList(); - - return StackedColumnSeries, String>( - dataSource: data, - xValueMapper: (d, _) => d['date'], - yValueMapper: (d, _) => d['present'], - name: role, - legendIconType: LegendIconType.circle, - dataLabelSettings: const DataLabelSettings(isVisible: true), - dataLabelMapper: (d, _) => - d['present'] == 0 ? '' : d['present'].toString(), - color: _getRoleColor(role), - ); - }).toList(), - ), - ); - } - - Widget _buildNoDataMessage() { - return SizedBox( - height: 200, - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.info_outline, color: Colors.grey.shade500, size: 48), - const SizedBox(height: 12), - MyText.bodyMedium( - 'No attendance data available for the selected range or project.', - textAlign: TextAlign.center, - color: Colors.grey.shade600, - ), - ], - ), - ), - ); - } - - Widget _buildTable() { - final formattedDateMap = { - for (var e in filteredData) - '${e['role']}_${DateFormat('d MMMM').format(DateTime.parse(e['date']))}': - e['present'] - }; - - return Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(12), - ), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: DataTable( - columnSpacing: 28, - headingRowHeight: 42, - headingRowColor: - WidgetStateProperty.all(Colors.blueAccent.withOpacity(0.1)), - headingTextStyle: const TextStyle( - fontWeight: FontWeight.bold, color: Colors.black87), - columns: [ - DataColumn(label: MyText.labelSmall('Role', fontWeight: 600)), - ...filteredDates.map((date) => DataColumn( - label: MyText.labelSmall(date, fontWeight: 600), - )), - ], - rows: filteredRoles.map((role) { - return DataRow( - cells: [ - DataCell(Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: _rolePill(role), - )), - ...filteredDates.map((date) { - final key = '${role}_$date'; - return DataCell(Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: MyText.labelSmall('${formattedDateMap[key] ?? 0}'), - )); - }), - ], - ); - }).toList(), - ), - ), - ); - } - - Widget _rolePill(String role) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - decoration: BoxDecoration( - color: _getRoleColor(role).withOpacity(0.15), - borderRadius: BorderRadius.circular(8), - ), - child: MyText.labelSmall(role, fontWeight: 500), - ); - } -} diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index fe88941..a6a0d3a 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -9,10 +9,12 @@ import 'package:marco/helpers/widgets/my_card.dart'; import 'package:marco/helpers/widgets/my_container.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; -import 'package:marco/view/dashboard/dashboard_chart.dart'; +import 'package:marco/helpers/widgets/dashbaord/attendance_overview_chart.dart'; +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 @@ -76,82 +78,53 @@ 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), + _buildProjectProgressChartSection(), ], ), ), ); } - /// Dashboard Statistics Section with ProjectController, Obx reactivity for menus - Widget _buildDashboardStats(BuildContext context) { + /// Project Progress Chart Section + Widget _buildProjectProgressChartSection() { return Obx(() { - if (menuController.isLoading.value) { - return _buildLoadingSkeleton(context); - } - if (menuController.hasError.value) { + if (dashboardController.isProjectLoading.value) { return Padding( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(8.0), + child: SkeletonLoaders.chartSkeletonLoader(), + ); + } + + if (dashboardController.projectChartData.isEmpty) { + return const Padding( + padding: EdgeInsets.all(16), child: Center( - child: MyText.bodySmall( - "Failed to load menus. Please try again later.", - color: Colors.red, - ), + child: Text("No project progress data available."), ), ); } - final stats = [ - _StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success, - DashboardScreen.attendanceRoute), - _StatItem(LucideIcons.users, "Employees", contentTheme.warning, - DashboardScreen.employeesRoute), - _StatItem(LucideIcons.logs, "Daily Task Planning", contentTheme.info, - DashboardScreen.dailyTasksRoute), - _StatItem(LucideIcons.list_todo, "Daily Progress Report", - contentTheme.info, DashboardScreen.dailyTasksProgressRoute), - _StatItem(LucideIcons.folder, "Directory", contentTheme.info, - DashboardScreen.directoryMainPageRoute), - _StatItem(LucideIcons.badge_dollar_sign, "Expense", contentTheme.info, - DashboardScreen.expenseMainPageRoute), - ]; - - final projectController = Get.find(); - final isProjectSelected = projectController.selectedProject != null; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isProjectSelected) _buildNoProjectMessage(), - LayoutBuilder( - builder: (context, constraints) { - final maxWidth = constraints.maxWidth; - final crossAxisCount = (maxWidth / 100).floor().clamp(2, 4); - final cardWidth = - (maxWidth - (crossAxisCount - 1) * 10) / crossAxisCount; - - return Wrap( - spacing: 10, - runSpacing: 10, - children: stats - .where((stat) { - final isAllowed = - menuController.isMenuAllowed(stat.title); - - // 👇 Log what is being checked - debugPrint( - "[Dashboard Menu] Checking menu: ${stat.title} -> Allowed: $isAllowed"); - - return isAllowed; - }) - .map((stat) => - _buildStatCard(stat, cardWidth, isProjectSelected)) - .toList(), - ); - }, + return ClipRRect( + borderRadius: BorderRadius.circular(12), + child: SizedBox( + height: 400, + child: ProjectProgressChart( + data: dashboardController.projectChartData, ), - ], + ), ); }); } @@ -163,7 +136,8 @@ class _DashboardScreenState extends State with UIMixin { // ✅ Show Skeleton Loader Instead of CircularProgressIndicator return Padding( padding: const EdgeInsets.all(8.0), - child: SkeletonLoaders.chartSkeletonLoader(), // <-- using the skeleton we built + child: SkeletonLoaders + .chartSkeletonLoader(), // <-- using the skeleton we built ); } @@ -184,7 +158,10 @@ class _DashboardScreenState extends State with UIMixin { ignoring: !isProjectSelected, child: ClipRRect( borderRadius: BorderRadius.circular(12), - child: AttendanceDashboardChart(), + child: SizedBox( + height: 400, + child: AttendanceDashboardChart(), + ), ), ), ); @@ -259,30 +236,95 @@ class _DashboardScreenState extends State with UIMixin { } /// Stat Card - Widget _buildStatCard(_StatItem statItem, double width, bool isEnabled) { + /// Dashboard Statistics Section with Compact Cards + Widget _buildDashboardStats(BuildContext context) { + return Obx(() { + if (menuController.isLoading.value) { + return _buildLoadingSkeleton(context); + } + if (menuController.hasError.value) { + return Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: MyText.bodySmall( + "Failed to load menus. Please try again later.", + color: Colors.red, + ), + ), + ); + } + + final stats = [ + _StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success, + DashboardScreen.attendanceRoute), + _StatItem(LucideIcons.users, "Employees", contentTheme.warning, + DashboardScreen.employeesRoute), + _StatItem(LucideIcons.logs, "Daily Task Planning", contentTheme.info, + DashboardScreen.dailyTasksRoute), + _StatItem(LucideIcons.list_todo, "Daily Progress Report", + contentTheme.info, DashboardScreen.dailyTasksProgressRoute), + _StatItem(LucideIcons.folder, "Directory", contentTheme.info, + DashboardScreen.directoryMainPageRoute), + _StatItem(LucideIcons.badge_dollar_sign, "Expense", contentTheme.info, + DashboardScreen.expenseMainPageRoute), + ]; + + final projectController = Get.find(); + final isProjectSelected = projectController.selectedProject != null; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isProjectSelected) _buildNoProjectMessage(), + Wrap( + spacing: 6, // horizontal spacing + runSpacing: 6, // vertical spacing + children: stats + .where((stat) => menuController.isMenuAllowed(stat.title)) + .map((stat) => _buildStatCard(stat, isProjectSelected)) + .toList(), + ), + ], + ); + }); + } + + /// Stat Card (Compact with wrapping text) + Widget _buildStatCard(_StatItem statItem, bool isEnabled) { + const double cardWidth = 80; + const double cardHeight = 70; + return Opacity( opacity: isEnabled ? 1.0 : 0.4, child: IgnorePointer( ignoring: !isEnabled, child: InkWell( onTap: () => _handleStatCardTap(statItem, isEnabled), - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(8), child: MyCard.bordered( - width: width, - height: 100, - paddingAll: 5, - borderRadiusAll: 10, + width: cardWidth, + height: cardHeight, + paddingAll: 4, + borderRadiusAll: 8, border: Border.all(color: Colors.grey.withOpacity(0.15)), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - _buildStatCardIcon(statItem), - MySpacing.height(8), - MyText.labelSmall( - statItem.title, - maxLines: 2, - overflow: TextOverflow.visible, - textAlign: TextAlign.center, + _buildStatCardIconCompact(statItem), + MySpacing.height(4), + Expanded( + child: Center( + child: Text( + statItem.title, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 10, + overflow: TextOverflow.visible, + ), + maxLines: 2, + softWrap: true, + ), + ), ), ], ), @@ -292,6 +334,19 @@ class _DashboardScreenState extends State with UIMixin { ); } + /// Compact Icon + Widget _buildStatCardIconCompact(_StatItem statItem) { + return MyContainer.rounded( + paddingAll: 6, + color: statItem.color.withOpacity(0.1), + child: Icon( + statItem.icon, + size: 14, + color: statItem.color, + ), + ); + } + /// Handle Tap void _handleStatCardTap(_StatItem statItem, bool isEnabled) { if (!isEnabled) { @@ -308,15 +363,6 @@ class _DashboardScreenState extends State with UIMixin { Get.toNamed(statItem.route); } } - - /// Stat Icon - Widget _buildStatCardIcon(_StatItem statItem) { - return MyContainer.rounded( - paddingAll: 10, - color: statItem.color.withOpacity(0.1), - child: Icon(statItem.icon, size: 18, color: statItem.color), - ); - } } class _StatItem { diff --git a/lib/view/dashboard/employee_screen.dart b/lib/view/dashboard/employee_screen.dart deleted file mode 100644 index 8c20f49..0000000 --- a/lib/view/dashboard/employee_screen.dart +++ /dev/null @@ -1,244 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:marco/helpers/theme/app_theme.dart'; -import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; -import 'package:marco/helpers/widgets/my_breadcrumb.dart'; -import 'package:marco/helpers/widgets/my_breadcrumb_item.dart'; -import 'package:marco/helpers/widgets/my_flex.dart'; -import 'package:marco/helpers/widgets/my_flex_item.dart'; -import 'package:marco/helpers/widgets/my_spacing.dart'; -import 'package:marco/helpers/widgets/my_text.dart'; -import 'package:marco/view/layouts/layout.dart'; -import 'package:marco/controller/permission_controller.dart'; -import 'package:marco/controller/dashboard/employees_screen_controller.dart'; -import 'package:marco/helpers/widgets/my_loading_component.dart'; -import 'package:marco/helpers/widgets/my_refresh_wrapper.dart'; -import 'package:marco/model/my_paginated_table.dart'; - -class EmployeeScreen extends StatefulWidget { - const EmployeeScreen({super.key}); - - @override - State createState() => _EmployeeScreenState(); -} - -class _EmployeeScreenState extends State with UIMixin { - final EmployeesScreenController employeesScreenController = - Get.put(EmployeesScreenController()); - final PermissionController permissionController = - Get.put(PermissionController()); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) async { - employeesScreenController.selectedProjectId = null; - await employeesScreenController.fetchAllEmployees(); - employeesScreenController.update(); - }); - } - - @override - Widget build(BuildContext context) { - return Layout( - child: Obx(() { - return LoadingComponent( - isLoading: employeesScreenController.isLoading.value, - loadingText: 'Loading Employees...', - child: GetBuilder( - init: employeesScreenController, - builder: (controller) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: MySpacing.x(flexSpacing), - child: MyText.titleMedium("Employee", - fontSize: 18, fontWeight: 600), - ), - MySpacing.height(flexSpacing), - Padding( - padding: MySpacing.x(flexSpacing), - child: MyBreadcrumb( - children: [ - MyBreadcrumbItem(name: 'Dashboard'), - MyBreadcrumbItem(name: 'Employee', active: true), - ], - ), - ), - MySpacing.height(flexSpacing), - Padding( - padding: MySpacing.x(flexSpacing), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Colors.black, - width: 1.5, - ), - borderRadius: BorderRadius.circular(4), - ), - child: PopupMenuButton( - onSelected: (String value) async { - if (value.isEmpty) { - employeesScreenController.selectedProjectId = - null; - await employeesScreenController - .fetchAllEmployees(); - } else { - employeesScreenController.selectedProjectId = - value; - await employeesScreenController - .fetchEmployeesByProject(value); - } - employeesScreenController.update(); - }, - itemBuilder: (BuildContext context) { - List> items = [ - PopupMenuItem( - value: '', - child: MyText.bodySmall('All Employees', - fontWeight: 600), - ), - ]; - - items.addAll( - employeesScreenController.projects - .map>((project) { - return PopupMenuItem( - value: project.id, - child: MyText.bodySmall(project.name), - ); - }).toList(), - ); - - return items; - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, vertical: 8.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - employeesScreenController - .selectedProjectId == - null - ? 'All Employees' - : employeesScreenController.projects - .firstWhere((project) => - project.id == - employeesScreenController - .selectedProjectId) - .name, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.w600), - ), - ), - const SizedBox(width: 4), - const Icon( - Icons.arrow_drop_down, - size: 20, - color: Colors.black, - ), - ], - ), - ), - ), - ), - ), - const SizedBox(width: 10), - ElevatedButton( - onPressed: () { - Get.toNamed('/employees/addEmployee'); - }, - child: Text('Add New Employee'), - ), - ], - ), - ), - MySpacing.height(flexSpacing), - Padding( - padding: MySpacing.x(flexSpacing / 2), - child: MyFlex( - children: [ - MyFlexItem(sizes: 'lg-6 ', child: employeeListTab()), - ], - ), - ), - ], - ); - }, - ), - ); - }), - ); - } - - Widget employeeListTab() { - if (employeesScreenController.employees.isEmpty) { - return Center( - child: MyText.bodySmall("No Employees Assigned to This Project", - fontWeight: 600), - ); - } - - final columns = [ - DataColumn(label: MyText.labelLarge('Name', color: contentTheme.primary)), - DataColumn( - label: MyText.labelLarge('Contact', color: contentTheme.primary)), - ]; - - final rows = - employeesScreenController.employees.asMap().entries.map((entry) { - var employee = entry.value; - return DataRow( - cells: [ - DataCell( - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - MyText.bodyMedium(employee.name, fontWeight: 600), - const SizedBox(height: 2), - MyText.bodySmall(employee.jobRole, color: Colors.grey), - ], - ), - ), - DataCell( - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - MyText.bodyMedium(employee.email, fontWeight: 600), - const SizedBox(height: 2), - MyText.bodySmall(employee.phoneNumber, color: Colors.grey), - ], - ), - ), - ], - ); - }).toList(); - - return MyRefreshableContent( - onRefresh: () async { - if (employeesScreenController.selectedProjectId == null) { - await employeesScreenController.fetchAllEmployees(); - } else { - await employeesScreenController.fetchEmployeesByProject( - employeesScreenController.selectedProjectId!, - ); - } - }, - child: MyPaginatedTable( - columns: columns, - rows: rows, - ), - ); - } -} diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index c28beb6..27fb507 100644 --- a/lib/view/employees/employee_detail_screen.dart +++ b/lib/view/employees/employee_detail_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; -import 'package:marco/controller/dashboard/employees_screen_controller.dart'; +import 'package:marco/controller/employee/employees_screen_controller.dart'; import 'package:marco/controller/project_controller.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; @@ -14,8 +14,12 @@ import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; class EmployeeDetailPage extends StatefulWidget { final String employeeId; - - const EmployeeDetailPage({super.key, required this.employeeId}); + final bool fromProfile; + const EmployeeDetailPage({ + super.key, + required this.employeeId, + this.fromProfile = false, + }); @override State createState() => _EmployeeDetailPageState(); @@ -201,7 +205,13 @@ class _EmployeeDetailPageState extends State { IconButton( icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20), - onPressed: () => Get.offNamed('/dashboard/employees'), + onPressed: () { + if (widget.fromProfile) { + Get.back(); + } else { + Get.offNamed('/dashboard/employees'); + } + }, ), MySpacing.width(8), Expanded( diff --git a/lib/view/employees/employees_screen.dart b/lib/view/employees/employees_screen.dart index 66a1fa7..3feb40d 100644 --- a/lib/view/employees/employees_screen.dart +++ b/lib/view/employees/employees_screen.dart @@ -5,7 +5,7 @@ import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/model/employees/add_employee_bottom_sheet.dart'; -import 'package:marco/controller/dashboard/employees_screen_controller.dart'; +import 'package:marco/controller/employee/employees_screen_controller.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/controller/project_controller.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; diff --git a/lib/view/expense/expense_detail_screen.dart b/lib/view/expense/expense_detail_screen.dart index a8ccbf9..0aa774c 100644 --- a/lib/view/expense/expense_detail_screen.dart +++ b/lib/view/expense/expense_detail_screen.dart @@ -15,7 +15,7 @@ import 'package:marco/helpers/services/app_logger.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; -import 'package:marco/helpers/widgets/expense_detail_helpers.dart'; +import 'package:marco/helpers/widgets/expense/expense_detail_helpers.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; diff --git a/lib/view/expense/expense_screen.dart b/lib/view/expense/expense_screen.dart index 0c404eb..e7ce5e6 100644 --- a/lib/view/expense/expense_screen.dart +++ b/lib/view/expense/expense_screen.dart @@ -8,7 +8,7 @@ import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/model/expense/expense_list_model.dart'; import 'package:marco/model/expense/add_expense_bottom_sheet.dart'; import 'package:marco/view/expense/expense_filter_bottom_sheet.dart'; -import 'package:marco/helpers/widgets/expense_main_components.dart'; +import 'package:marco/helpers/widgets/expense/expense_main_components.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; diff --git a/lib/view/layouts/user_profile_right_bar.dart b/lib/view/layouts/user_profile_right_bar.dart index 74b7973..f739d42 100644 --- a/lib/view/layouts/user_profile_right_bar.dart +++ b/lib/view/layouts/user_profile_right_bar.dart @@ -1,325 +1,341 @@ +import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:marco/helpers/theme/theme_customizer.dart'; +import 'package:flutter_lucide/flutter_lucide.dart'; +import 'package:get/get.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; -import 'package:marco/helpers/utils/my_shadow.dart'; -import 'package:marco/helpers/widgets/my_card.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; -import 'package:flutter_lucide/flutter_lucide.dart'; -import 'package:marco/model/employees/employee_info.dart'; import 'package:marco/helpers/widgets/avatar.dart'; -import 'package:get/get.dart'; +import 'package:marco/model/employees/employee_info.dart'; import 'package:marco/controller/auth/mpin_controller.dart'; +import 'package:marco/view/employees/employee_detail_screen.dart'; class UserProfileBar extends StatefulWidget { final bool isCondensed; - - const UserProfileBar({super.key, this.isCondensed = false}); + const UserProfileBar({Key? key, this.isCondensed = false}) : super(key: key); @override - _UserProfileBarState createState() => _UserProfileBarState(); + State createState() => _UserProfileBarState(); } class _UserProfileBarState extends State with SingleTickerProviderStateMixin, UIMixin { - final ThemeCustomizer customizer = ThemeCustomizer.instance; - bool isCondensed = false; - EmployeeInfo? employeeInfo; + late EmployeeInfo employeeInfo; + bool _isLoading = true; bool hasMpin = true; @override void initState() { super.initState(); - _loadEmployeeInfo(); - _checkMpinStatus(); + _initData(); } - void _loadEmployeeInfo() { - setState(() { - employeeInfo = LocalStorage.getEmployeeInfo(); - }); - } - - Future _checkMpinStatus() async { - final bool mpinStatus = await LocalStorage.getIsMpin(); - setState(() { - hasMpin = mpinStatus; - }); + Future _initData() async { + employeeInfo = LocalStorage.getEmployeeInfo()!; + hasMpin = await LocalStorage.getIsMpin(); + setState(() => _isLoading = false); } @override Widget build(BuildContext context) { - isCondensed = widget.isCondensed; - - return MyCard( - borderRadiusAll: 16, - paddingAll: 0, - shadow: MyShadow( - position: MyShadowPosition.centerRight, - elevation: 6, - blurRadius: 12, - ), - child: AnimatedContainer( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - leftBarTheme.background.withOpacity(0.97), - leftBarTheme.background.withOpacity(0.88), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - width: isCondensed ? 90 : 260, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - child: SafeArea( - bottom: true, - top: false, - left: false, - right: false, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - userProfileSection(), - MySpacing.height(16), - supportAndSettingsMenu(), - const Spacer(), - logoutButton(), - ], - ), - ), - ), - ); - } - - /// User Profile Section - Avatar + Name - Widget userProfileSection() { - if (employeeInfo == null) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 32), - child: Center(child: CircularProgressIndicator()), - ); - } - - return Container( - width: double.infinity, - padding: MySpacing.fromLTRB(20, 50, 30, 50), - decoration: BoxDecoration( - color: leftBarTheme.activeItemBackground, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - child: Row( - children: [ - Avatar( - firstName: employeeInfo?.firstName ?? 'F', - lastName: employeeInfo?.lastName ?? 'N', - size: 50, - ), - MySpacing.width(12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodyMedium( - "${employeeInfo?.firstName ?? 'First'} ${employeeInfo?.lastName ?? 'Last'}", - fontWeight: 700, - color: leftBarTheme.activeItemColor, - ), + final bool isCondensed = widget.isCondensed; + return Padding( + padding: const EdgeInsets.only(left: 14), + child: ClipRRect( + borderRadius: BorderRadius.circular(22), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18), + child: AnimatedContainer( + duration: const Duration(milliseconds: 350), + curve: Curves.easeInOut, + width: isCondensed ? 84 : 260, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.white.withValues(alpha: 0.95), + Colors.white.withValues(alpha: 0.85), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(22), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 18, + offset: const Offset(0, 8), + ) ], + border: Border.all( + color: Colors.grey.withValues(alpha: 0.25), + width: 1, + ), + ), + child: SafeArea( + bottom: true, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _isLoading + ? const _LoadingSection() + : _userProfileSection(isCondensed), + MySpacing.height(12), + Divider( + indent: 18, + endIndent: 18, + thickness: 0.7, + color: Colors.grey.withValues(alpha: 0.25), + ), + MySpacing.height(12), + _supportAndSettingsMenu(isCondensed), + const Spacer(), + Divider( + indent: 18, + endIndent: 18, + thickness: 0.35, + color: Colors.grey.withValues(alpha: 0.18), + ), + _logoutButton(isCondensed), + ], + ), ), ), + ), + ), + ); + } + + Widget _userProfileSection(bool condensed) { + final padding = MySpacing.fromLTRB( + condensed ? 16 : 26, + condensed ? 20 : 28, + condensed ? 14 : 28, + condensed ? 10 : 18, + ); + final avatarSize = condensed ? 48.0 : 64.0; + + return Padding( + padding: padding, + child: Row( + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Theme.of(context).primaryColor.withValues(alpha: 0.15), + blurRadius: 10, + spreadRadius: 1, + ), + ], + border: Border.all( + color: Theme.of(context).primaryColor, + width: 2, + ), + ), + child: Avatar( + firstName: employeeInfo.firstName, + lastName: employeeInfo.lastName, + size: avatarSize, + ), + ), + if (!condensed) ...[ + MySpacing.width(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyLarge( + '${employeeInfo.firstName} ${employeeInfo.lastName}', + fontWeight: 700, + color: Colors.indigo, + ), + MySpacing.height(4), + MyText.bodySmall( + "You're on track this month!", + color: Colors.black54, + ), + ], + ), + ), + ], ], ), ); } - /// Menu Section with Settings, Support & MPIN - Widget supportAndSettingsMenu() { + Widget _supportAndSettingsMenu(bool condensed) { + final spacingHeight = condensed ? 8.0 : 14.0; return Padding( - padding: MySpacing.xy(16, 16), + padding: const EdgeInsets.symmetric(horizontal: 14), child: Column( children: [ - menuItem( + _menuItemRow( + icon: LucideIcons.user, + label: 'My Profile', + onTap: _onProfileTap, + ), + SizedBox(height: spacingHeight), + _menuItemRow( icon: LucideIcons.settings, - label: "Settings", + label: 'Settings', ), - MySpacing.height(14), - menuItem( + SizedBox(height: spacingHeight), + _menuItemRow( icon: LucideIcons.badge_help, - label: "Support", + label: 'Support', ), - MySpacing.height(14), - menuItem( + SizedBox(height: spacingHeight), + _menuItemRow( icon: LucideIcons.lock, - label: hasMpin ? "Change MPIN" : "Set MPIN", - iconColor: hasMpin ? leftBarTheme.onBackground : Colors.redAccent, - labelColor: hasMpin ? leftBarTheme.onBackground : Colors.redAccent, - onTap: () { - final controller = Get.put(MPINController()); - if (hasMpin) { - controller.setChangeMpinMode(); - } - Navigator.pushNamed(context, "/auth/mpin-auth"); - }, - filled: true, + label: hasMpin ? 'Change MPIN' : 'Set MPIN', + iconColor: Colors.redAccent, + textColor: Colors.redAccent, + onTap: _onMpinTap, ), ], ), ); } - Widget menuItem({ + Widget _menuItemRow({ required IconData icon, required String label, - Color? iconColor, - Color? labelColor, VoidCallback? onTap, - bool filled = false, + Color? iconColor, + Color? textColor, }) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), - hoverColor: leftBarTheme.activeItemBackground.withOpacity(0.25), - splashColor: leftBarTheme.activeItemBackground.withOpacity(0.35), child: Container( - padding: MySpacing.xy(14, 12), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( - color: filled - ? leftBarTheme.activeItemBackground.withOpacity(0.15) - : Colors.transparent, + color: Colors.white.withOpacity(0.9), borderRadius: BorderRadius.circular(12), - border: Border.all( - color: filled - ? leftBarTheme.activeItemBackground.withOpacity(0.3) - : Colors.transparent, - width: 1, - ), + border: Border.all(color: Colors.grey.withOpacity(0.2), width: 1), ), child: Row( children: [ - Icon(icon, size: 22, color: iconColor ?? leftBarTheme.onBackground), - MySpacing.width(14), + Icon(icon, size: 22, color: iconColor ?? Colors.black87), + const SizedBox(width: 16), Expanded( - child: MyText.bodyMedium( + child: Text( label, - color: labelColor ?? leftBarTheme.onBackground, - fontWeight: 600, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: textColor ?? Colors.black87, + ), ), ), + const Icon(Icons.chevron_right, size: 20, color: Colors.black54), ], ), ), ); } - /// Logout Button - Widget logoutButton() { - return InkWell( - onTap: () async { - await _showLogoutConfirmation(); - }, - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(16), - bottomRight: Radius.circular(16), - ), - hoverColor: leftBarTheme.activeItemBackground.withOpacity(0.25), - splashColor: leftBarTheme.activeItemBackground.withOpacity(0.35), - child: Container( - padding: MySpacing.all(16), - decoration: BoxDecoration( - color: leftBarTheme.activeItemBackground, - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(16), - bottomRight: Radius.circular(16), + void _onProfileTap() { + Get.to(() => EmployeeDetailPage( + employeeId: employeeInfo.id, + fromProfile: true, + )); + } + + void _onMpinTap() { + final controller = Get.put(MPINController()); + if (hasMpin) controller.setChangeMpinMode(); + Navigator.pushNamed(context, "/auth/mpin-auth"); + } + + Widget _logoutButton(bool condensed) { + return Padding( + padding: MySpacing.all(14), + child: SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _showLogoutConfirmation, + icon: const Icon(LucideIcons.log_out, size: 22, color: Colors.white), + label: condensed + ? const SizedBox.shrink() + : MyText.bodyMedium( + "Logout", + color: Colors.white, + fontWeight: 700, + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red.shade600, + foregroundColor: Colors.white, + shadowColor: Colors.red.shade200, + padding: EdgeInsets.symmetric( + vertical: condensed ? 14 : 18, + horizontal: condensed ? 14 : 22, + ), + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - MyText.bodyMedium( - "Logout", - color: leftBarTheme.activeItemColor, - fontWeight: 600, - ), - MySpacing.width(8), - Icon( - LucideIcons.log_out, - size: 20, - color: leftBarTheme.activeItemColor, - ), - ], - ), ), ); } Future _showLogoutConfirmation() async { - bool? confirm = await showDialog( + final bool? confirm = await showDialog( context: context, - builder: (context) => _buildLogoutDialog(context), + builder: _buildLogoutDialog, ); - - if (confirm == true) { - await LocalStorage.logout(); - } + if (confirm == true) await LocalStorage.logout(); } Widget _buildLogoutDialog(BuildContext context) { return Dialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + elevation: 10, + backgroundColor: Colors.white, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28), + padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 34), child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(LucideIcons.log_out, size: 48, color: Colors.redAccent), - const SizedBox(height: 16), - Text( + Icon(LucideIcons.log_out, size: 56, color: Colors.red.shade700), + const SizedBox(height: 18), + const Text( "Logout Confirmation", style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.onBackground, - ), + fontSize: 22, + fontWeight: FontWeight.w700, + color: Colors.black87), ), - const SizedBox(height: 12), - Text( + const SizedBox(height: 14), + const Text( "Are you sure you want to logout?\nYou will need to login again to continue.", textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + style: TextStyle(fontSize: 16, color: Colors.black54), ), - const SizedBox(height: 24), + const SizedBox(height: 30), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: TextButton( onPressed: () => Navigator.pop(context, false), style: TextButton.styleFrom( - foregroundColor: Colors.grey.shade700, - ), + foregroundColor: Colors.grey.shade700, + padding: const EdgeInsets.symmetric(vertical: 12)), child: const Text("Cancel"), ), ), - const SizedBox(width: 12), + const SizedBox(width: 18), Expanded( child: ElevatedButton( onPressed: () => Navigator.pop(context, true), style: ElevatedButton.styleFrom( - backgroundColor: Colors.redAccent, + backgroundColor: Colors.red.shade700, foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12), + padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + borderRadius: BorderRadius.circular(14)), ), child: const Text("Logout"), ), @@ -332,3 +348,15 @@ class _UserProfileBarState extends State ); } } + +class _LoadingSection extends StatelessWidget { + const _LoadingSection(); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 44, horizontal: 8), + child: Center(child: CircularProgressIndicator()), + ); + } +} diff --git a/lib/view/taskPlanning/daily_progress.dart b/lib/view/taskPlanning/daily_progress.dart index 3513281..868ab7b 100644 --- a/lib/view/taskPlanning/daily_progress.dart +++ b/lib/view/taskPlanning/daily_progress.dart @@ -9,7 +9,7 @@ import 'package:marco/helpers/widgets/my_container.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/controller/permission_controller.dart'; -import 'package:marco/controller/dashboard/daily_task_controller.dart'; +import 'package:marco/controller/task_planning/daily_task_controller.dart'; import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/controller/project_controller.dart';