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..89074b3 100644 --- a/lib/controller/dashboard/dashboard_controller.dart +++ b/lib/controller/dashboard/dashboard_controller.dart @@ -2,15 +2,32 @@ 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; - // Inject the ProjectController + final RxString attendanceSelectedRange = '15D'.obs; + final RxBool attendanceIsChartView = true.obs; + final RxBool isAttendanceLoading = false.obs; + + // ========================= + // Project progress overview + // ========================= + final RxList projectChartData = [].obs; + + final RxString projectSelectedRange = '15D'.obs; + final RxBool projectIsChartView = true.obs; + final RxBool isProjectLoading = false.obs; + + // Common ranges + final List ranges = ['7D', '15D', '30D']; + + // Inject ProjectController final ProjectController projectController = Get.find(); @override @@ -20,77 +37,113 @@ class DashboardController extends GetxController { logSafe( 'DashboardController initialized with project ID: ${projectController.selectedProjectId.value}', level: LogLevel.info, - ); if (projectController.selectedProjectId.value.isNotEmpty) { fetchRoleWiseAttendance(); + fetchProjectProgress(); } // React to project change ever(projectController.selectedProjectId, (id) { if (id.isNotEmpty) { - logSafe('Project changed to $id, fetching attendance', level: LogLevel.info, ); + logSafe('Project changed to $id, refreshing dashboard', + level: LogLevel.info); fetchRoleWiseAttendance(); + fetchProjectProgress(); } }); - // React to range change - ever(selectedRange, (_) { - fetchRoleWiseAttendance(); - }); + // 👇 Separate listeners for ranges + ever(attendanceSelectedRange, (_) => fetchRoleWiseAttendance()); + ever(projectSelectedRange, (_) => fetchProjectProgress()); } - int get rangeDays => _getDaysFromRange(selectedRange.value); + // ========================= + // Helpers + // ========================= 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 Future.wait([ + fetchRoleWiseAttendance(), + fetchProjectProgress(), + ]); } + // ========================= + // API Calls + // ========================= + Future fetchRoleWiseAttendance() async { final String projectId = projectController.selectedProjectId.value; if (projectId.isEmpty) { - logSafe('Project ID is empty, skipping API call.', level: LogLevel.warning); + logSafe('Project ID is empty, skipping attendance API call.', + level: LogLevel.warning); 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(); @@ -101,7 +154,48 @@ class DashboardController extends GetxController { stackTrace: st, ); } finally { - isLoading.value = false; + isAttendanceLoading.value = false; + } + } + + Future fetchProjectProgress() async { + final String projectId = projectController.selectedProjectId.value; + + if (projectId.isEmpty) { + logSafe('Project ID is empty, skipping project progress API call.', + level: LogLevel.warning); + return; + } + + try { + isProjectLoading.value = true; + + // Instead of using DateTime.now(), we simply pass 'days' to the API + // Let the backend decide the correct date range + final response = await ApiService.getProjectProgress( + projectId: projectId, + days: getProjectDays(), + // Remove fromDate + ); + + if (response != null && response.success) { + 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; } } } 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..3b09a34 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -4,6 +4,7 @@ class ApiEndpoints { // Dashboard Module API Endpoints static const String getDashboardAttendanceOverview = "/dashboard/attendance-overview"; + static const String getDashboardProjectProgress = "/dashboard/progression"; // 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..c8c54be 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -8,7 +8,7 @@ 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/helpers/services/app_logger.dart'; class ApiService { @@ -625,6 +625,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/dashbaord/attendance_overview_chart.dart b/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart new file mode 100644 index 0000000..4368458 --- /dev/null +++ b/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart @@ -0,0 +1,464 @@ +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) +]; + + + // Cache role colors as static to maintain immutability in stateless widget context + 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 as a separate widget for clarity & reusability +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: [ + // Title + toggle row + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium('Attendance Overview', fontWeight: 700), + 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), + // Range buttons + 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 message widget +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, + ), + ], + ), + ), + ); + } +} + +// Attendance Chart widget +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(); + + // Check if all present values are zero + 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), + ), + ), + ); + } + + // Normal chart rendering + 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}; + }).toList(); + + return StackedColumnSeries, String>( + dataSource: seriesData, + xValueMapper: (d, _) => d['date'], + yValueMapper: (d, _) => d['present'], + name: role, + color: getRoleColor(role), + dataLabelSettings: const DataLabelSettings( + isVisible: true, + textStyle: TextStyle(fontSize: 11), + ), + ); + }).toList(), + ), + ); + } +} + +// Attendance Table widget +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(); + + // Check if all present values are zero + 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), + ), + ), + ); + } + + // Normal table rendering + 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('${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/project_progress_chart.dart b/lib/helpers/widgets/dashbaord/project_progress_chart.dart new file mode 100644 index 0000000..5874b51 --- /dev/null +++ b/lib/helpers/widgets/dashbaord/project_progress_chart.dart @@ -0,0 +1,344 @@ +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 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), + primaryXAxis: DateTimeAxis( + dateFormat: DateFormat('MMM d'), + intervalType: DateTimeIntervalType.days, + majorGridLines: MajorGridLines(width: 0), + ), + primaryYAxis: NumericAxis( + labelFormat: '{value}', + axisLine: AxisLine(width: 0), + majorTickLines: MajorTickLines(size: 0), + ), + series: [ + ColumnSeries( + name: 'Planned', + dataSource: nonZeroData, + xValueMapper: (d, _) => d.date, + yValueMapper: (d, _) => d.planned, + color: _getTaskColor('Planned'), + dataLabelSettings: DataLabelSettings( + isVisible: true, textStyle: TextStyle(fontSize: 11)), + ), + ColumnSeries( + name: 'Completed', + dataSource: nonZeroData, + xValueMapper: (d, _) => d.date, + yValueMapper: (d, _) => d.completed, + color: _getTaskColor('Completed'), + dataLabelSettings: DataLabelSettings( + isVisible: true, textStyle: 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/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/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..1e810f8 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -9,7 +9,8 @@ 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'; @@ -78,80 +79,41 @@ class _DashboardScreenState extends State with UIMixin { _buildDashboardStats(context), 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 +125,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 +147,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 +225,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 +323,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 +352,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/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index c28beb6..431fe4f 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'; diff --git a/lib/view/dashboard/employee_screen.dart b/lib/view/employees/employee_screen.dart similarity index 99% rename from lib/view/dashboard/employee_screen.dart rename to lib/view/employees/employee_screen.dart index 8c20f49..5a5acae 100644 --- a/lib/view/dashboard/employee_screen.dart +++ b/lib/view/employees/employee_screen.dart @@ -10,7 +10,7 @@ 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/controller/employee/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'; 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/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';