diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json index 0b9a014..f595601 100644 --- a/.dart_tool/package_config.json +++ b/.dart_tool/package_config.json @@ -97,6 +97,12 @@ "packageUri": "lib/", "languageVersion": "3.3" }, + { + "name": "equatable", + "rootUri": "file:///C:/Users/PC/AppData/Local/Pub/Cache/hosted/pub.dev/equatable-2.0.7", + "packageUri": "lib/", + "languageVersion": "2.12" + }, { "name": "fake_async", "rootUri": "file:///C:/Users/PC/AppData/Local/Pub/Cache/hosted/pub.dev/fake_async-1.3.3", @@ -169,6 +175,12 @@ "packageUri": "lib/", "languageVersion": "3.4" }, + { + "name": "fl_chart", + "rootUri": "file:///C:/Users/PC/AppData/Local/Pub/Cache/hosted/pub.dev/fl_chart-0.67.0", + "packageUri": "lib/", + "languageVersion": "3.2" + }, { "name": "flutter", "rootUri": "file:///C:/flutter/packages/flutter", diff --git a/.dart_tool/package_graph.json b/.dart_tool/package_graph.json index ad7fefb..61358bd 100644 --- a/.dart_tool/package_graph.json +++ b/.dart_tool/package_graph.json @@ -11,6 +11,7 @@ "dio", "firebase_auth", "firebase_core", + "fl_chart", "flutter", "flutter_dotenv", "flutter_web_plugins", @@ -180,6 +181,14 @@ "meta" ] }, + { + "name": "fl_chart", + "version": "0.67.0", + "dependencies": [ + "equatable", + "flutter" + ] + }, { "name": "flutter_web_plugins", "version": "0.0.0", @@ -565,6 +574,14 @@ "plugin_platform_interface" ] }, + { + "name": "equatable", + "version": "2.0.7", + "dependencies": [ + "collection", + "meta" + ] + }, { "name": "sky_engine", "version": "0.0.0", diff --git a/build/7e4aebe516b998635f34742713e086a8.cache.dill.track.dill b/build/7e4aebe516b998635f34742713e086a8.cache.dill.track.dill index e1ce933..6dec7dd 100644 Binary files a/build/7e4aebe516b998635f34742713e086a8.cache.dill.track.dill and b/build/7e4aebe516b998635f34742713e086a8.cache.dill.track.dill differ diff --git a/build/flutter_assets/NOTICES b/build/flutter_assets/NOTICES index e6766c1..ea059d2 100644 --- a/build/flutter_assets/NOTICES +++ b/build/flutter_assets/NOTICES @@ -3925,6 +3925,31 @@ which is quoted below: full, permanent, irrevocable, nonexclusive and worldwide license for all her or his copyright and related or neighboring legal rights in the Work. +-------------------------------------------------------------------------------- +equatable + +MIT License + +Copyright (c) 2024 Felix Angelov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + -------------------------------------------------------------------------------- etc_decoder @@ -5385,6 +5410,31 @@ firebase_core_web // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------------------- +fl_chart + +MIT License + +Copyright (c) 2022 Flutter 4 Fun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + -------------------------------------------------------------------------------- flatbuffers diff --git a/lib/controllers/homepage_controller.dart b/lib/controllers/homepage_controller.dart index 8139c5e..910b789 100644 --- a/lib/controllers/homepage_controller.dart +++ b/lib/controllers/homepage_controller.dart @@ -130,7 +130,7 @@ class HomePageController extends ChangeNotifier { ? p.consultingType : 'Sin categoría'; - final DateTime? endDate = p.endDate; + final DateTime endDate = p.endDate; final String dueLabel = endDate != null ? '${endDate.day.toString().padLeft(2, '0')}/' '${endDate.month.toString().padLeft(2, '0')}/' diff --git a/lib/controllers/resources_controller.dart b/lib/controllers/resources_controller.dart index 6de8733..6cf55b7 100644 --- a/lib/controllers/resources_controller.dart +++ b/lib/controllers/resources_controller.dart @@ -644,7 +644,7 @@ class ResourcesController { for (final doc in humanSnap.docs) { if (doc.exists) { - final data = doc.data() as Map; + final data = doc.data(); if (data.containsKey('name')) { final int currentUse = (data['use'] as num? ?? 0).toInt(); final int total = (data['totalUsage'] as num? ?? 0).toInt(); diff --git a/lib/controllers/task_controller.dart b/lib/controllers/task_controller.dart index e741131..aa86f43 100644 --- a/lib/controllers/task_controller.dart +++ b/lib/controllers/task_controller.dart @@ -575,6 +575,55 @@ class TaskController extends ChangeNotifier { ]; } + // ===================== REPORTING LOGIC (Task Count by Status) ===================== + + Map _countTasksByStatus(List tasks) { + final Map counts = {}; + for (var task in tasks) { + final statusKey = _statusToString(task.status); + counts[statusKey] = (counts[statusKey] ?? 0) + 1; + } + return counts; + } + + Stream> streamTaskStatusCounts() { + if (_currentProjectId == null) { + return const Stream>.empty(); + } + + return _firestore + .collection('projects') + .doc(_currentProjectId!) + .collection('tasks') + .snapshots() + .map((snapshot) { + final List tasks = snapshot.docs.map((doc) { + final Map data = doc.data(); + return Task( + id: doc.id, + title: data['title'] ?? '', + description: data['description'] ?? '', + projectType: data['projectType'] ?? '', + assignee: data['assignee'] ?? '', + priority: _stringToPriority( + (data['priority'] ?? 'MEDIUM').toString(), + ), + status: _stringToStatus( + (data['status'] ?? 'pendiente').toString(), + ), + estimatedHours: (data['estimatedHours'] ?? 0).toDouble(), + dueTime: data['dueDate'] != null + ? DateTime.parse(data['dueDate'] as String) + : null, + tags: List.from(data['tags'] ?? []), + projectId: _currentProjectId!, + ); + }).toList(); + + return _countTasksByStatus(tasks); + }); + } + // ===== MAPPERS: PRIORITY & STATUS ===== Priority _stringToPriority(String priority) { diff --git a/lib/core/routes/app_routes.dart b/lib/core/routes/app_routes.dart index db72242..f75b0b0 100644 --- a/lib/core/routes/app_routes.dart +++ b/lib/core/routes/app_routes.dart @@ -14,7 +14,6 @@ import 'package:prolab_unimet/views/task_view.dart'; import 'package:provider/provider.dart'; import 'package:prolab_unimet/views/dashboard_view.dart'; import 'package:prolab_unimet/views/help_module_view.dart'; -import 'package:prolab_unimet/controllers/settings_controller.dart'; // Define user roles for authorization const userRoles = ['USER', 'ADMIN', 'COORDINATOR']; @@ -96,7 +95,7 @@ final appRouter = GoRouter( ), GoRoute( path: '/admin-reports', - builder: (context, state) => const ReportsView(), + builder: (context, state) => ReportsView(), redirect: (context, state) => _requireAuth(context, userRoles), ), ], diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index df43030..998b64d 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -48,7 +48,7 @@ class AuthService { 'role': role, 'token': token, }; - } on fb_auth.FirebaseAuthException catch (e) { + } on fb_auth.FirebaseAuthException { rethrow; } catch (e) { // Non-Firebase auth error (network, Firestore, etc.) @@ -137,7 +137,7 @@ class AuthService { /// Returns a fresh ID token for the current user, if any. Future getToken() async { final fb_auth.User? user = _auth.currentUser; - return user != null ? user.getIdToken() : null; + return user?.getIdToken(); } /// Signs out the current user. diff --git a/lib/views/components/resources/assign_resource.dialog.dart b/lib/views/components/resources/assign_resource.dialog.dart index 62df042..5785962 100644 --- a/lib/views/components/resources/assign_resource.dialog.dart +++ b/lib/views/components/resources/assign_resource.dialog.dart @@ -138,7 +138,7 @@ class _AssignResourceDialogState extends State { final List nombres = snap.data!; return DropdownButtonFormField( - value: _selectedProject, + initialValue: _selectedProject, items: nombres .map( (nombre) => DropdownMenuItem( @@ -166,7 +166,7 @@ class _AssignResourceDialogState extends State { const Text('Prioridad *'), const SizedBox(height: 4), DropdownButtonFormField( - value: _selectedPriority, + initialValue: _selectedPriority, items: const [ DropdownMenuItem(value: 'Alta', child: Text('Alta')), DropdownMenuItem(value: 'Media', child: Text('Media')), diff --git a/lib/views/help_module_view.dart b/lib/views/help_module_view.dart index 62ca17f..a08c6f5 100644 --- a/lib/views/help_module_view.dart +++ b/lib/views/help_module_view.dart @@ -407,6 +407,15 @@ class _FaqItem extends StatelessWidget { ), ), + trailing: Icon( + isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, + color: Theme.of(context).colorScheme.primary, + ), + tilePadding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 4.0, + ), + children: [ Padding( padding: const EdgeInsets.only( @@ -424,15 +433,6 @@ class _FaqItem extends StatelessWidget { ), ), ], - - trailing: Icon( - isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, - color: Theme.of(context).colorScheme.primary, - ), - tilePadding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 4.0, - ), ), ), ); @@ -594,7 +594,7 @@ class _QuickLinksSection extends StatelessWidget { ), ), ); - }).toList(), + }), ], ), ); diff --git a/lib/views/reports/reports_view.dart b/lib/views/reports/reports_view.dart index c15b02d..b702b1e 100644 --- a/lib/views/reports/reports_view.dart +++ b/lib/views/reports/reports_view.dart @@ -1,8 +1,41 @@ import 'package:flutter/material.dart'; +import 'package:prolab_unimet/controllers/task_controller.dart'; import 'package:provider/provider.dart'; import 'package:prolab_unimet/controllers/reports_controller.dart'; import 'package:prolab_unimet/models/projects_model.dart'; import 'package:prolab_unimet/models/reports_model.dart'; +import 'package:fl_chart/fl_chart.dart'; + +const Map taskStatusMapping = { + 'pendiente': (name: 'Pendiente', color: Color(0xFF9E9E9E), index: 0), + 'enProgreso': (name: 'En Progreso', color: Color(0xFF2196F3), index: 1), + 'enRevision': (name: 'En Revisión', color: Color(0xFFFF9800), index: 2), + 'completado': (name: 'Completado', color: Color(0xFF4CAF50), index: 3), +}; + +List getTaskBarGroups(Map taskCounts) { + return taskStatusMapping.entries.map((entry) { + final statusKey = entry.key; + final info = entry.value; + final count = taskCounts[statusKey] ?? 0; + + return BarChartGroupData( + x: info.index, + barRods: [ + BarChartRodData( + toY: count.toDouble(), + color: info.color, + width: 25, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(6), + topRight: Radius.circular(6), + ), + ), + ], + showingTooltipIndicators: count > 0 ? [0] : [], + ); + }).toList(); +} /// Main view for reports and analytics. class ReportsView extends StatelessWidget { @@ -48,6 +81,8 @@ class ReportsView extends StatelessWidget { _buildStatsRow(context, model), const SizedBox(height: 25), _buildGenerateReportCard(context, controller, model), + const SizedBox(height: 25), + _buildTaskStatusBarChart(context), ], ], ), @@ -91,6 +126,131 @@ class ReportsView extends StatelessWidget { ); } + Widget _buildTaskStatusBarChart(BuildContext context) { + final taskController = Provider.of(context, listen: false); + final screenWidth = MediaQuery.of(context).size.width; + const double maxWidth = 600.0; + final double chartWidth = screenWidth * 0.95 > maxWidth + ? maxWidth + : screenWidth * 0.95; + + if (taskController.currentProjectId == null) { + return const Center( + child: Padding( + padding: EdgeInsets.all(20.0), + child: Text( + 'Selecciona un proyecto para ver la distribución de tareas.', + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + ), + ); + } + + return StreamBuilder>( + stream: taskController.streamTaskStatusCounts(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: LinearProgressIndicator()); + } + + final counts = snapshot.data ?? {}; + final barGroups = getTaskBarGroups(counts); + + final maxY = counts.values.isEmpty + ? 10.0 + : counts.values.reduce((a, b) => a > b ? a : b) + 2.0; + + return Center( + child: SizedBox( + width: chartWidth, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Distribución de Tareas por Estado', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Color(0xFF1E3A8A), + ), + ), + const SizedBox(height: 15), + Container( + height: 250, + padding: const EdgeInsets.only( + top: 10, + right: 10, + left: 10, + bottom: 0, + ), + child: BarChart( + BarChartData( + maxY: maxY, + alignment: BarChartAlignment.spaceAround, + groupsSpace: 12, + barTouchData: BarTouchData(enabled: false), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + getTitlesWidget: (value, meta) { + final statusInfo = taskStatusMapping.values + .firstWhere( + (e) => e.index == value.toInt(), + orElse: () => ( + name: '', + color: Colors.transparent, + index: -1, + ), + ); + return SideTitleWidget( + axisSide: meta.axisSide, + space: 4.0, + child: Text( + statusInfo.name, + style: const TextStyle(fontSize: 10), + ), + ); + }, + ), + ), + leftTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + gridData: const FlGridData(show: false), + borderData: FlBorderData( + show: true, + border: const Border( + bottom: BorderSide( + color: Color(0xff37434d), + width: 1, + ), + left: BorderSide.none, + right: BorderSide.none, + top: BorderSide.none, + ), + ), + barGroups: barGroups, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + /// Card with button to open the generate-report dialog. Widget _buildGenerateReportCard( BuildContext context, @@ -321,7 +481,7 @@ class _GenerateProjectReportDialogState borderRadius: BorderRadius.circular(8), ), ), - value: _selectedProject, + initialValue: _selectedProject, items: projects.map((project) { final title = project.name.isEmpty ? 'Sin título' diff --git a/lib/views/tasks/add_task_dialog.dart b/lib/views/tasks/add_task_dialog.dart index 12e490a..3b81074 100644 --- a/lib/views/tasks/add_task_dialog.dart +++ b/lib/views/tasks/add_task_dialog.dart @@ -163,7 +163,8 @@ class _AddTaskState extends State { ) else DropdownButtonFormField( - value: _selectedProjectId ?? widget.projectId, + initialValue: + _selectedProjectId ?? widget.projectId, items: availableProjects.map((project) { final String id = project['id'] as String? ?? ''; final String name = @@ -249,7 +250,8 @@ class _AddTaskState extends State { ) else DropdownButtonFormField( - value: _selectedAssigneeId ?? members.first['id'], + initialValue: + _selectedAssigneeId ?? members.first['id'], items: members.map((member) { return DropdownMenuItem( value: member['id'], @@ -279,7 +281,7 @@ class _AddTaskState extends State { children: [ const Text('Prioridad', style: TextStyle(fontSize: 14)), DropdownButtonFormField( - value: _selectedPriority, + initialValue: _selectedPriority, items: Priority.values.map((priority) { return DropdownMenuItem( value: priority, @@ -305,7 +307,7 @@ class _AddTaskState extends State { children: [ const Text('Estado', style: TextStyle(fontSize: 14)), DropdownButtonFormField( - value: _selectedStatus, + initialValue: _selectedStatus, items: Status.values.map((status) { return DropdownMenuItem( value: status, @@ -347,7 +349,7 @@ class _AddTaskState extends State { children: [ const Text('Columna', style: TextStyle(fontSize: 14)), DropdownButtonFormField( - value: _selectedColumn, + initialValue: _selectedColumn, items: columns.map((column) { return DropdownMenuItem( value: column, diff --git a/pubspec.lock b/pubspec.lock index 3d54d2c..9d801d2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -129,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -225,6 +233,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "2b7c1f5d867da9a054661641c8f499c55c47c39acccb97b3bc673f5fa9a39e74" + url: "https://pub.dev" + source: hosted + version: "0.67.0" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 10821d5..5addf00 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: flutter_web_plugins: sdk: flutter + fl_chart: ^0.67.0 firebase_auth: ^6.0.0 provider: ^6.1.5 firebase_core: ^4.1.1