diff --git a/lib/main.dart b/lib/main.dart index 50a19ec..3ab0ba7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,29 +1,47 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'services/theme_provider.dart'; +import 'utils/theme.dart'; +import 'screens/student/home_screen.dart'; +import 'screens/student/submit_grievance_screen.dart'; +import 'screens/student/my_grievances_screen.dart'; +import 'screens/student/profile_screen.dart'; void main() { runApp(CollegeGrievanceApp()); } class CollegeGrievanceApp extends StatelessWidget { + const CollegeGrievanceApp({super.key}); + @override Widget build(BuildContext context) { - return MaterialApp( - title: 'College Grievance System', - theme: ThemeData( - primarySwatch: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, + return ChangeNotifierProvider( + create: (context) => ThemeProvider(), + child: Consumer( + builder: (context, themeProvider, child) { + return MaterialApp( + title: 'College Grievance System', + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + themeMode: themeProvider.themeMode, + home: MainScreen(), + debugShowCheckedModeBanner: false, + ); + }, ), - home: MainScreen(), ); } } class MainScreen extends StatefulWidget { + const MainScreen({super.key}); + @override - _MainScreenState createState() => _MainScreenState(); + MainScreenState createState() => MainScreenState(); } -class _MainScreenState extends State { +class MainScreenState extends State { int _currentIndex = 0; final List _screens = [ @@ -46,10 +64,7 @@ class _MainScreenState extends State { }, type: BottomNavigationBarType.fixed, items: [ - BottomNavigationBarItem( - icon: Icon(Icons.home), - label: 'Home', - ), + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), BottomNavigationBarItem( icon: Icon(Icons.add_circle), label: 'Submit', @@ -58,914 +73,7 @@ class _MainScreenState extends State { icon: Icon(Icons.list), label: 'My Grievances', ), - BottomNavigationBarItem( - icon: Icon(Icons.person), - label: 'Profile', - ), - ], - ), - ); - } -} - -class HomeScreen extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('College Grievance System'), - backgroundColor: Colors.blue[600], - foregroundColor: Colors.white, - ), - body: SingleChildScrollView( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Card( - elevation: 4, - child: Container( - width: double.infinity, - padding: EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [Colors.blue[400]!, Colors.blue[600]!], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Welcome to Grievance Portal', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - SizedBox(height: 8), - Text( - 'Submit your problems and track their resolution', - style: TextStyle( - fontSize: 16, - color: Colors.white70, - ), - ), - ], - ), - ), - ), - SizedBox(height: 24), - Text( - 'Quick Actions', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.grey[800], - ), - ), - SizedBox(height: 16), - GridView.count( - crossAxisCount: 2, - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - crossAxisSpacing: 16, - mainAxisSpacing: 16, - children: [ - _buildQuickActionCard( - 'Academic Issues', - Icons.school, - Colors.orange, - context, - ), - _buildQuickActionCard( - 'Hostel Problems', - Icons.home, - Colors.green, - context, - ), - _buildQuickActionCard( - 'Library Issues', - Icons.library_books, - Colors.purple, - context, - ), - _buildQuickActionCard( - 'Canteen Complaints', - Icons.restaurant, - Colors.red, - context, - ), - _buildQuickActionCard( - 'Transport Issues', - Icons.directions_bus, - Colors.teal, - context, - ), - _buildQuickActionCard( - 'Other Issues', - Icons.more_horiz, - Colors.grey, - context, - ), - ], - ), - ], - ), - ), - ); - } - - Widget _buildQuickActionCard(String title, IconData icon, Color color, BuildContext context) { - return Card( - elevation: 2, - child: InkWell( - onTap: () { - // Navigate to submit grievance screen - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SubmitGrievanceScreen(category: title), - ), - ); - }, - child: Container( - padding: EdgeInsets.all(16), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - icon, - size: 40, - color: color, - ), - SizedBox(height: 8), - Text( - title, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.grey[800], - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ), - ); - } -} - -class SubmitGrievanceScreen extends StatefulWidget { - final String? category; - - SubmitGrievanceScreen({this.category}); - - @override - _SubmitGrievanceScreenState createState() => _SubmitGrievanceScreenState(); -} - -class _SubmitGrievanceScreenState extends State { - final _formKey = GlobalKey(); - final _nameController = TextEditingController(); - final _idController = TextEditingController(); - final _emailController = TextEditingController(); - final _phoneController = TextEditingController(); - final _descriptionController = TextEditingController(); - - String? _selectedCategory; - String? _selectedDepartment; - String? _selectedYear; - String? _selectedPriority; - - final List _categories = [ - 'Academic Issues', - 'Hostel Problems', - 'Library Issues', - 'Canteen Complaints', - 'Transport Issues', - 'Administrative Problems', - 'Technical Issues', - 'Other', - ]; - - final List _departments = [ - 'Computer Science', - 'Mechanical Engineering', - 'Electrical Engineering', - 'Civil Engineering', - 'Electronics & Communication', - 'Information Technology', - 'Business Administration', - 'Commerce', - 'Arts', - 'Science', - ]; - - final List _years = [ - '1st Year', - '2nd Year', - '3rd Year', - '4th Year', - 'Final Year', - ]; - - final List _priorities = [ - 'Low', - 'Medium', - 'High', - 'Urgent', - ]; - - @override - void initState() { - super.initState(); - if (widget.category != null) { - _selectedCategory = widget.category; - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('Submit Grievance'), - backgroundColor: Colors.blue[600], - foregroundColor: Colors.white, - ), - body: SingleChildScrollView( - padding: EdgeInsets.all(16.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Student Information', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey[800], - ), - ), - SizedBox(height: 16), - TextFormField( - controller: _nameController, - decoration: InputDecoration( - labelText: 'Full Name', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.person), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your name'; - } - return null; - }, - ), - SizedBox(height: 16), - TextFormField( - controller: _idController, - decoration: InputDecoration( - labelText: 'Student ID', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.badge), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your student ID'; - } - return null; - }, - ), - SizedBox(height: 16), - TextFormField( - controller: _emailController, - decoration: InputDecoration( - labelText: 'Email Address', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.email), - ), - keyboardType: TextInputType.emailAddress, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your email'; - } - if (!value.contains('@')) { - return 'Please enter a valid email'; - } - return null; - }, - ), - SizedBox(height: 16), - TextFormField( - controller: _phoneController, - decoration: InputDecoration( - labelText: 'Phone Number', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.phone), - ), - keyboardType: TextInputType.phone, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your phone number'; - } - return null; - }, - ), - SizedBox(height: 16), - DropdownButtonFormField( - value: _selectedDepartment, - decoration: InputDecoration( - labelText: 'Department', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.account_balance), - ), - items: _departments.map((String department) { - return DropdownMenuItem( - value: department, - child: Text(department), - ); - }).toList(), - onChanged: (String? newValue) { - setState(() { - _selectedDepartment = newValue; - }); - }, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please select your department'; - } - return null; - }, - ), - SizedBox(height: 16), - DropdownButtonFormField( - value: _selectedYear, - decoration: InputDecoration( - labelText: 'Year of Study', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.class_), - ), - items: _years.map((String year) { - return DropdownMenuItem( - value: year, - child: Text(year), - ); - }).toList(), - onChanged: (String? newValue) { - setState(() { - _selectedYear = newValue; - }); - }, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please select your year of study'; - } - return null; - }, - ), - SizedBox(height: 24), - Text( - 'Grievance Details', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.grey[800], - ), - ), - SizedBox(height: 16), - DropdownButtonFormField( - value: _selectedCategory, - decoration: InputDecoration( - labelText: 'Category', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.category), - ), - items: _categories.map((String category) { - return DropdownMenuItem( - value: category, - child: Text(category), - ); - }).toList(), - onChanged: (String? newValue) { - setState(() { - _selectedCategory = newValue; - }); - }, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please select a category'; - } - return null; - }, - ), - SizedBox(height: 16), - DropdownButtonFormField( - value: _selectedPriority, - decoration: InputDecoration( - labelText: 'Priority Level', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.priority_high), - ), - items: _priorities.map((String priority) { - return DropdownMenuItem( - value: priority, - child: Text(priority), - ); - }).toList(), - onChanged: (String? newValue) { - setState(() { - _selectedPriority = newValue; - }); - }, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please select priority level'; - } - return null; - }, - ), - SizedBox(height: 16), - TextFormField( - controller: _descriptionController, - decoration: InputDecoration( - labelText: 'Describe your problem in detail', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.description), - ), - maxLines: 5, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please describe your problem'; - } - return null; - }, - ), - SizedBox(height: 16), - OutlinedButton.icon( - onPressed: () { - // File attachment functionality - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('File attachment feature will be implemented'), - ), - ); - }, - icon: Icon(Icons.attach_file), - label: Text('Attach File (Optional)'), - ), - SizedBox(height: 32), - Container( - width: double.infinity, - height: 50, - child: ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - // Process form submission - _submitGrievance(); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue[600], - foregroundColor: Colors.white, - ), - child: Text( - 'Submit Grievance', - style: TextStyle(fontSize: 16), - ), - ), - ), - ], - ), - ), - ), - ); - } - - void _submitGrievance() { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text('Success!'), - content: Text('Your grievance has been submitted successfully. You will receive a confirmation email shortly.'), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - Navigator.of(context).pop(); - }, - child: Text('OK'), - ), - ], - ); - }, - ); - } -} - -class MyGrievancesScreen extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('My Grievances'), - backgroundColor: Colors.blue[600], - foregroundColor: Colors.white, - ), - body: ListView.builder( - padding: EdgeInsets.all(16.0), - itemCount: 5, // Sample data - itemBuilder: (context, index) { - return Card( - elevation: 2, - margin: EdgeInsets.only(bottom: 16), - child: ListTile( - leading: CircleAvatar( - backgroundColor: _getStatusColor(index), - child: Icon( - _getStatusIcon(index), - color: Colors.white, - ), - ), - title: Text('Grievance #${1000 + index}'), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Category: ${_getCategory(index)}'), - Text('Status: ${_getStatus(index)}'), - Text('Date: ${_getDate(index)}'), - ], - ), - trailing: Icon(Icons.arrow_forward_ios), - onTap: () { - // Navigate to grievance detail screen - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => GrievanceDetailScreen( - grievanceId: 1000 + index, - ), - ), - ); - }, - ), - ); - }, - ), - ); - } - - Color _getStatusColor(int index) { - switch (index % 4) { - case 0: - return Colors.green; - case 1: - return Colors.orange; - case 2: - return Colors.red; - default: - return Colors.blue; - } - } - - IconData _getStatusIcon(int index) { - switch (index % 4) { - case 0: - return Icons.check_circle; - case 1: - return Icons.hourglass_empty; - case 2: - return Icons.error; - default: - return Icons.info; - } - } - - String _getCategory(int index) { - final categories = [ - 'Academic Issues', - 'Hostel Problems', - 'Library Issues', - 'Canteen Complaints', - 'Transport Issues', - ]; - return categories[index % categories.length]; - } - - String _getStatus(int index) { - final statuses = ['Resolved', 'In Progress', 'Pending', 'Under Review']; - return statuses[index % statuses.length]; - } - - String _getDate(int index) { - final dates = [ - '2024-01-15', - '2024-01-10', - '2024-01-05', - '2023-12-28', - '2023-12-20', - ]; - return dates[index % dates.length]; - } -} - -class GrievanceDetailScreen extends StatelessWidget { - final int grievanceId; - - GrievanceDetailScreen({required this.grievanceId}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('Grievance #$grievanceId'), - backgroundColor: Colors.blue[600], - foregroundColor: Colors.white, - ), - body: SingleChildScrollView( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Card( - child: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Grievance Details', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 16), - _buildDetailRow('ID:', '#$grievanceId'), - _buildDetailRow('Category:', 'Academic Issues'), - _buildDetailRow('Priority:', 'High'), - _buildDetailRow('Status:', 'In Progress'), - _buildDetailRow('Submitted:', '2024-01-15'), - _buildDetailRow('Last Updated:', '2024-01-18'), - ], - ), - ), - ), - SizedBox(height: 16), - Card( - child: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Description', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 8), - Text( - 'This is a sample grievance description. The actual description would contain the details of the problem submitted by the student.', - style: TextStyle(fontSize: 14), - ), - ], - ), - ), - ), - SizedBox(height: 16), - Card( - child: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Status Timeline', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 16), - _buildTimelineItem('Submitted', '2024-01-15 10:30 AM', true), - _buildTimelineItem('Under Review', '2024-01-16 02:15 PM', true), - _buildTimelineItem('In Progress', '2024-01-18 11:45 AM', true), - _buildTimelineItem('Resolved', 'Pending', false), - ], - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildDetailRow(String label, String value) { - return Padding( - padding: EdgeInsets.only(bottom: 8.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 100, - child: Text( - label, - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.grey[600], - ), - ), - ), - Expanded( - child: Text(value), - ), - ], - ), - ); - } - - Widget _buildTimelineItem(String title, String time, bool isCompleted) { - return Row( - children: [ - Container( - width: 20, - height: 20, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: isCompleted ? Colors.green : Colors.grey[300], - ), - child: isCompleted - ? Icon(Icons.check, color: Colors.white, size: 14) - : null, - ), - SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - fontWeight: FontWeight.bold, - color: isCompleted ? Colors.black : Colors.grey[600], - ), - ), - Text( - time, - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - ), - ), - ], - ), - ), - ], - ); - } -} - -class ProfileScreen extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('Profile'), - backgroundColor: Colors.blue[600], - foregroundColor: Colors.white, - ), - body: SingleChildScrollView( - padding: EdgeInsets.all(16.0), - child: Column( - children: [ - CircleAvatar( - radius: 50, - backgroundColor: Colors.blue[100], - child: Icon( - Icons.person, - size: 60, - color: Colors.blue[600], - ), - ), - SizedBox(height: 16), - Text( - 'John Doe', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - Text( - 'Student ID: 2021CS001', - style: TextStyle( - fontSize: 16, - color: Colors.grey[600], - ), - ), - SizedBox(height: 32), - Card( - child: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Personal Information', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 16), - _buildInfoRow('Email:', 'john.doe@college.edu'), - _buildInfoRow('Phone:', '+1 234 567 8900'), - _buildInfoRow('Department:', 'Computer Science'), - _buildInfoRow('Year:', '3rd Year'), - _buildInfoRow('Roll Number:', '2021CS001'), - ], - ), - ), - ), - SizedBox(height: 16), - Card( - child: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Grievance Statistics', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 16), - _buildInfoRow('Total Submitted:', '5'), - _buildInfoRow('Resolved:', '2'), - _buildInfoRow('In Progress:', '2'), - _buildInfoRow('Pending:', '1'), - ], - ), - ), - ), - SizedBox(height: 32), - Container( - width: double.infinity, - height: 50, - child: ElevatedButton( - onPressed: () { - // Edit profile functionality - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Edit profile feature will be implemented'), - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue[600], - foregroundColor: Colors.white, - ), - child: Text( - 'Edit Profile', - style: TextStyle(fontSize: 16), - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildInfoRow(String label, String value) { - return Padding( - padding: EdgeInsets.only(bottom: 8.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 120, - child: Text( - label, - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.grey[600], - ), - ), - ), - Expanded( - child: Text(value), - ), + BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'), ], ), ); diff --git a/lib/models/analytics.dart b/lib/models/analytics.dart new file mode 100644 index 0000000..9b1e79b --- /dev/null +++ b/lib/models/analytics.dart @@ -0,0 +1,101 @@ +class Analytics { + final int totalGrievances; + final int resolvedGrievances; + final int pendingGrievances; + final int inProgressGrievances; + final double averageResolutionTime; + final Map grievancesByCategory; + final Map grievancesByDepartment; + final Map grievancesByStatus; + final Map grievancesByPriority; + final List monthlyStats; + + Analytics({ + required this.totalGrievances, + required this.resolvedGrievances, + required this.pendingGrievances, + required this.inProgressGrievances, + required this.averageResolutionTime, + required this.grievancesByCategory, + required this.grievancesByDepartment, + required this.grievancesByStatus, + required this.grievancesByPriority, + required this.monthlyStats, + }); + + // Computed properties for admin dashboard compatibility + int get pendingCount => pendingGrievances; + int get underReviewCount => grievancesByStatus['underReview'] ?? 0; + int get inProgressCount => inProgressGrievances; + int get resolvedCount => resolvedGrievances; + int get rejectedCount => grievancesByStatus['closed'] ?? 0; + double get resolutionRate => + totalGrievances > 0 ? resolvedCount / totalGrievances : 0.0; + + Map toJson() { + return { + 'totalGrievances': totalGrievances, + 'resolvedGrievances': resolvedGrievances, + 'pendingGrievances': pendingGrievances, + 'inProgressGrievances': inProgressGrievances, + 'averageResolutionTime': averageResolutionTime, + 'grievancesByCategory': grievancesByCategory, + 'grievancesByDepartment': grievancesByDepartment, + 'grievancesByStatus': grievancesByStatus, + 'grievancesByPriority': grievancesByPriority, + 'monthlyStats': monthlyStats.map((s) => s.toJson()).toList(), + }; + } + + factory Analytics.fromJson(Map json) { + return Analytics( + totalGrievances: json['totalGrievances'], + resolvedGrievances: json['resolvedGrievances'], + pendingGrievances: json['pendingGrievances'], + inProgressGrievances: json['inProgressGrievances'], + averageResolutionTime: json['averageResolutionTime'].toDouble(), + grievancesByCategory: Map.from(json['grievancesByCategory']), + grievancesByDepartment: Map.from( + json['grievancesByDepartment'], + ), + grievancesByStatus: Map.from(json['grievancesByStatus']), + grievancesByPriority: Map.from(json['grievancesByPriority']), + monthlyStats: + (json['monthlyStats'] as List) + .map((s) => MonthlyStats.fromJson(s)) + .toList(), + ); + } +} + +class MonthlyStats { + final String month; + final int year; + final int totalSubmitted; + final int totalResolved; + + MonthlyStats({ + required this.month, + required this.year, + required this.totalSubmitted, + required this.totalResolved, + }); + + Map toJson() { + return { + 'month': month, + 'year': year, + 'totalSubmitted': totalSubmitted, + 'totalResolved': totalResolved, + }; + } + + factory MonthlyStats.fromJson(Map json) { + return MonthlyStats( + month: json['month'], + year: json['year'], + totalSubmitted: json['totalSubmitted'], + totalResolved: json['totalResolved'], + ); + } +} diff --git a/lib/models/grievance.dart b/lib/models/grievance.dart new file mode 100644 index 0000000..6bde183 --- /dev/null +++ b/lib/models/grievance.dart @@ -0,0 +1,189 @@ +enum GrievanceStatus { pending, underReview, inProgress, resolved, closed } + +enum Priority { low, medium, high, urgent } + +class Grievance { + final String id; + final String title; + final String description; + final String category; + final Priority priority; + final GrievanceStatus status; + final String studentId; + final String studentName; + final String studentEmail; + final String department; + final String year; + final DateTime submittedDate; + final DateTime? lastUpdated; + final DateTime? resolvedDate; + final List attachments; + final List statusHistory; + final String? assignedTo; + final int? rating; + final String? feedback; + + Grievance({ + required this.id, + required this.title, + required this.description, + required this.category, + required this.priority, + required this.status, + required this.studentId, + required this.studentName, + required this.studentEmail, + required this.department, + required this.year, + required this.submittedDate, + this.lastUpdated, + this.resolvedDate, + this.attachments = const [], + this.statusHistory = const [], + this.assignedTo, + this.rating, + this.feedback, + }); + + Map toJson() { + return { + 'id': id, + 'title': title, + 'description': description, + 'category': category, + 'priority': priority.name, + 'status': status.name, + 'studentId': studentId, + 'studentName': studentName, + 'studentEmail': studentEmail, + 'department': department, + 'year': year, + 'submittedDate': submittedDate.toIso8601String(), + 'lastUpdated': lastUpdated?.toIso8601String(), + 'resolvedDate': resolvedDate?.toIso8601String(), + 'attachments': attachments, + 'statusHistory': statusHistory.map((s) => s.toJson()).toList(), + 'assignedTo': assignedTo, + 'rating': rating, + 'feedback': feedback, + }; + } + + factory Grievance.fromJson(Map json) { + return Grievance( + id: json['id'], + title: json['title'], + description: json['description'], + category: json['category'], + priority: Priority.values.firstWhere((p) => p.name == json['priority']), + status: GrievanceStatus.values.firstWhere( + (s) => s.name == json['status'], + ), + studentId: json['studentId'], + studentName: json['studentName'], + studentEmail: json['studentEmail'], + department: json['department'], + year: json['year'], + submittedDate: DateTime.parse(json['submittedDate']), + lastUpdated: + json['lastUpdated'] != null + ? DateTime.parse(json['lastUpdated']) + : null, + resolvedDate: + json['resolvedDate'] != null + ? DateTime.parse(json['resolvedDate']) + : null, + attachments: List.from(json['attachments'] ?? []), + statusHistory: + (json['statusHistory'] as List?) + ?.map((s) => StatusUpdate.fromJson(s)) + .toList() ?? + [], + assignedTo: json['assignedTo'], + rating: json['rating'], + feedback: json['feedback'], + ); + } + + Grievance copyWith({ + String? id, + String? title, + String? description, + String? category, + Priority? priority, + GrievanceStatus? status, + String? studentId, + String? studentName, + String? studentEmail, + String? department, + String? year, + DateTime? submittedDate, + DateTime? lastUpdated, + DateTime? resolvedDate, + List? attachments, + List? statusHistory, + String? assignedTo, + int? rating, + String? feedback, + }) { + return Grievance( + id: id ?? this.id, + title: title ?? this.title, + description: description ?? this.description, + category: category ?? this.category, + priority: priority ?? this.priority, + status: status ?? this.status, + studentId: studentId ?? this.studentId, + studentName: studentName ?? this.studentName, + studentEmail: studentEmail ?? this.studentEmail, + department: department ?? this.department, + year: year ?? this.year, + submittedDate: submittedDate ?? this.submittedDate, + lastUpdated: lastUpdated ?? this.lastUpdated, + resolvedDate: resolvedDate ?? this.resolvedDate, + attachments: attachments ?? this.attachments, + statusHistory: statusHistory ?? this.statusHistory, + assignedTo: assignedTo ?? this.assignedTo, + rating: rating ?? this.rating, + feedback: feedback ?? this.feedback, + ); + } +} + +class StatusUpdate { + final String id; + final GrievanceStatus status; + final String comment; + final DateTime timestamp; + final String updatedBy; + + StatusUpdate({ + required this.id, + required this.status, + required this.comment, + required this.timestamp, + required this.updatedBy, + }); + + Map toJson() { + return { + 'id': id, + 'status': status.name, + 'comment': comment, + 'timestamp': timestamp.toIso8601String(), + 'updatedBy': updatedBy, + }; + } + + factory StatusUpdate.fromJson(Map json) { + return StatusUpdate( + id: json['id'], + status: GrievanceStatus.values.firstWhere( + (s) => s.name == json['status'], + ), + comment: json['comment'], + timestamp: DateTime.parse(json['timestamp']), + updatedBy: json['updatedBy'], + ); + } +} diff --git a/lib/models/notification.dart b/lib/models/notification.dart new file mode 100644 index 0000000..03dfe00 --- /dev/null +++ b/lib/models/notification.dart @@ -0,0 +1,77 @@ +class AppNotification { + final String id; + final String title; + final String message; + final String userId; + final String? grievanceId; + final DateTime timestamp; + final bool isRead; + final NotificationType type; + + AppNotification({ + required this.id, + required this.title, + required this.message, + required this.userId, + this.grievanceId, + required this.timestamp, + this.isRead = false, + required this.type, + }); + + Map toJson() { + return { + 'id': id, + 'title': title, + 'message': message, + 'userId': userId, + 'grievanceId': grievanceId, + 'timestamp': timestamp.toIso8601String(), + 'isRead': isRead, + 'type': type.name, + }; + } + + factory AppNotification.fromJson(Map json) { + return AppNotification( + id: json['id'], + title: json['title'], + message: json['message'], + userId: json['userId'], + grievanceId: json['grievanceId'], + timestamp: DateTime.parse(json['timestamp']), + isRead: json['isRead'] ?? false, + type: NotificationType.values.firstWhere((t) => t.name == json['type']), + ); + } + + AppNotification copyWith({ + String? id, + String? title, + String? message, + String? userId, + String? grievanceId, + DateTime? timestamp, + bool? isRead, + NotificationType? type, + }) { + return AppNotification( + id: id ?? this.id, + title: title ?? this.title, + message: message ?? this.message, + userId: userId ?? this.userId, + grievanceId: grievanceId ?? this.grievanceId, + timestamp: timestamp ?? this.timestamp, + isRead: isRead ?? this.isRead, + type: type ?? this.type, + ); + } +} + +enum NotificationType { + statusUpdate, + newGrievance, + assignment, + reminder, + general, +} diff --git a/lib/models/user.dart b/lib/models/user.dart new file mode 100644 index 0000000..8e060c1 --- /dev/null +++ b/lib/models/user.dart @@ -0,0 +1,96 @@ +enum UserRole { student, admin, staff } + +class User { + final String id; + final String name; + final String email; + final String phone; + final UserRole role; + final String? studentId; + final String? department; + final String? year; + final String? profilePicture; + final DateTime createdAt; + final DateTime? lastLogin; + final bool isActive; + + User({ + required this.id, + required this.name, + required this.email, + required this.phone, + required this.role, + this.studentId, + this.department, + this.year, + this.profilePicture, + required this.createdAt, + this.lastLogin, + this.isActive = true, + }); + + Map toJson() { + return { + 'id': id, + 'name': name, + 'email': email, + 'phone': phone, + 'role': role.name, + 'studentId': studentId, + 'department': department, + 'year': year, + 'profilePicture': profilePicture, + 'createdAt': createdAt.toIso8601String(), + 'lastLogin': lastLogin?.toIso8601String(), + 'isActive': isActive, + }; + } + + factory User.fromJson(Map json) { + return User( + id: json['id'], + name: json['name'], + email: json['email'], + phone: json['phone'], + role: UserRole.values.firstWhere((r) => r.name == json['role']), + studentId: json['studentId'], + department: json['department'], + year: json['year'], + profilePicture: json['profilePicture'], + createdAt: DateTime.parse(json['createdAt']), + lastLogin: + json['lastLogin'] != null ? DateTime.parse(json['lastLogin']) : null, + isActive: json['isActive'] ?? true, + ); + } + + User copyWith({ + String? id, + String? name, + String? email, + String? phone, + UserRole? role, + String? studentId, + String? department, + String? year, + String? profilePicture, + DateTime? createdAt, + DateTime? lastLogin, + bool? isActive, + }) { + return User( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + phone: phone ?? this.phone, + role: role ?? this.role, + studentId: studentId ?? this.studentId, + department: department ?? this.department, + year: year ?? this.year, + profilePicture: profilePicture ?? this.profilePicture, + createdAt: createdAt ?? this.createdAt, + lastLogin: lastLogin ?? this.lastLogin, + isActive: isActive ?? this.isActive, + ); + } +} diff --git a/lib/screens/admin/admin_analytics_screen.dart b/lib/screens/admin/admin_analytics_screen.dart new file mode 100644 index 0000000..e0997b9 --- /dev/null +++ b/lib/screens/admin/admin_analytics_screen.dart @@ -0,0 +1,402 @@ +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; +import '../../models/analytics.dart'; +import '../../services/analytics_service.dart'; +import '../../widgets/stat_card.dart'; + +class AdminAnalyticsScreen extends StatefulWidget { + const AdminAnalyticsScreen({super.key}); + + @override + State createState() => _AdminAnalyticsScreenState(); +} + +class _AdminAnalyticsScreenState extends State { + Analytics? _analytics; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadAnalytics(); + } + + Future _loadAnalytics() async { + setState(() { + _isLoading = true; + }); + + try { + _analytics = await AnalyticsService.getOverallAnalytics(); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error loading analytics: $e'), + backgroundColor: Colors.red, + ), + ); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Analytics Dashboard'), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _loadAnalytics, + ), + ], + ), + body: + _isLoading + ? const Center(child: CircularProgressIndicator()) + : RefreshIndicator( + onRefresh: _loadAnalytics, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildOverviewStats(), + const SizedBox(height: 24), + _buildStatusChart(), + const SizedBox(height: 24), + _buildCategoryChart(), + const SizedBox(height: 24), + _buildMonthlyTrends(), + ], + ), + ), + ), + ); + } + + Widget _buildOverviewStats() { + if (_analytics == null) return const SizedBox.shrink(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Overview Statistics', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 2, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + childAspectRatio: 1.5, + children: [ + StatCard( + title: 'Total Grievances', + value: _analytics!.totalGrievances.toString(), + icon: Icons.list_alt, + color: Colors.blue, + ), + StatCard( + title: 'Pending', + value: _analytics!.pendingCount.toString(), + icon: Icons.hourglass_empty, + color: Colors.orange, + isUrgent: _analytics!.pendingCount > 5, + ), + StatCard( + title: 'Resolved', + value: _analytics!.resolvedCount.toString(), + icon: Icons.check_circle, + color: Colors.green, + ), + StatCard( + title: 'Avg Resolution Time', + value: + '${_analytics!.averageResolutionTime.toStringAsFixed(1)} days', + icon: Icons.timer, + color: Colors.purple, + isUrgent: _analytics!.averageResolutionTime > 7, + ), + ], + ), + ], + ); + } + + Widget _buildStatusChart() { + if (_analytics == null) return const SizedBox.shrink(); + + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Status Distribution', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + SizedBox( + height: 250, + child: PieChart( + PieChartData( + sections: [ + PieChartSectionData( + value: _analytics!.pendingCount.toDouble(), + title: 'Pending\n${_analytics!.pendingCount}', + color: Colors.orange, + radius: 100, + ), + PieChartSectionData( + value: _analytics!.underReviewCount.toDouble(), + title: 'Review\n${_analytics!.underReviewCount}', + color: Colors.blue, + radius: 100, + ), + PieChartSectionData( + value: _analytics!.inProgressCount.toDouble(), + title: 'Progress\n${_analytics!.inProgressCount}', + color: Colors.purple, + radius: 100, + ), + PieChartSectionData( + value: _analytics!.resolvedCount.toDouble(), + title: 'Resolved\n${_analytics!.resolvedCount}', + color: Colors.green, + radius: 100, + ), + ], + centerSpaceRadius: 50, + sectionsSpace: 2, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildCategoryChart() { + if (_analytics == null) return const SizedBox.shrink(); + + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Category Distribution', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + SizedBox( + height: 200, + child: BarChart( + BarChartData( + alignment: BarChartAlignment.spaceAround, + maxY: + _analytics!.grievancesByCategory.values.isNotEmpty + ? _analytics!.grievancesByCategory.values + .reduce((a, b) => a > b ? a : b) + .toDouble() + + 1 + : 10, + barTouchData: BarTouchData(enabled: false), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + final categories = + _analytics!.grievancesByCategory.keys.toList(); + if (value.toInt() >= 0 && + value.toInt() < categories.length) { + return Text( + categories[value.toInt()], + style: const TextStyle(fontSize: 10), + ); + } + return const Text(''); + }, + reservedSize: 30, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 40, + ), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + borderData: FlBorderData(show: false), + barGroups: + _analytics!.grievancesByCategory.entries + .toList() + .asMap() + .entries + .map((entry) { + return BarChartGroupData( + x: entry.key, + barRods: [ + BarChartRodData( + toY: entry.value.value.toDouble(), + color: Colors.blueAccent, + width: 16, + ), + ], + ); + }) + .toList(), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildMonthlyTrends() { + if (_analytics == null || _analytics!.monthlyStats.isEmpty) { + return const SizedBox.shrink(); + } + + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Monthly Trends', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + SizedBox( + height: 200, + child: LineChart( + LineChartData( + gridData: const FlGridData(show: true), + titlesData: FlTitlesData( + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + final index = value.toInt(); + if (index >= 0 && + index < _analytics!.monthlyStats.length) { + return Text( + _analytics!.monthlyStats[index].month, + style: const TextStyle(fontSize: 10), + ); + } + return const Text(''); + }, + reservedSize: 30, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 40, + ), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + borderData: FlBorderData(show: true), + minX: 0, + maxX: _analytics!.monthlyStats.length.toDouble() - 1, + minY: 0, + maxY: + _analytics!.monthlyStats + .map((s) => s.totalSubmitted) + .reduce((a, b) => a > b ? a : b) + .toDouble() + + 2, + lineBarsData: [ + LineChartBarData( + spots: + _analytics!.monthlyStats + .asMap() + .entries + .map( + (entry) => FlSpot( + entry.key.toDouble(), + entry.value.totalSubmitted.toDouble(), + ), + ) + .toList(), + isCurved: true, + color: Colors.blue, + barWidth: 3, + dotData: const FlDotData(show: true), + ), + LineChartBarData( + spots: + _analytics!.monthlyStats + .asMap() + .entries + .map( + (entry) => FlSpot( + entry.key.toDouble(), + entry.value.totalResolved.toDouble(), + ), + ) + .toList(), + isCurved: true, + color: Colors.green, + barWidth: 3, + dotData: const FlDotData(show: true), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildLegendItem('Submitted', Colors.blue), + const SizedBox(width: 24), + _buildLegendItem('Resolved', Colors.green), + ], + ), + ], + ), + ), + ); + } + + Widget _buildLegendItem(String label, Color color) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container(width: 12, height: 12, color: color), + const SizedBox(width: 4), + Text(label, style: const TextStyle(fontSize: 12)), + ], + ); + } +} diff --git a/lib/screens/admin/admin_dashboard_screen.dart b/lib/screens/admin/admin_dashboard_screen.dart new file mode 100644 index 0000000..41a1186 --- /dev/null +++ b/lib/screens/admin/admin_dashboard_screen.dart @@ -0,0 +1,453 @@ +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; +import '../../models/grievance.dart'; +import '../../models/analytics.dart'; +import '../../services/grievance_service.dart'; +import '../../services/analytics_service.dart'; +import '../../widgets/stat_card.dart'; +import '../../widgets/quick_action_card.dart'; +import '../../widgets/grievance_card.dart'; +import 'admin_grievances_screen.dart'; +import 'admin_analytics_screen.dart'; + +class AdminDashboardScreen extends StatefulWidget { + const AdminDashboardScreen({super.key}); + + @override + State createState() => _AdminDashboardScreenState(); +} + +class _AdminDashboardScreenState extends State { + Analytics? _analytics; + List _recentGrievances = []; + List _pendingGrievances = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadDashboardData(); + } + + Future _loadDashboardData() async { + setState(() { + _isLoading = true; + }); + + try { + _analytics = await AnalyticsService.getOverallAnalytics(); + + final allGrievances = GrievanceService.getAllGrievances(); + allGrievances.sort((a, b) => b.submittedDate.compareTo(a.submittedDate)); + + _recentGrievances = allGrievances.take(5).toList(); + _pendingGrievances = + allGrievances + .where((g) => g.status == GrievanceStatus.pending) + .take(3) + .toList(); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error loading dashboard: $e'), + backgroundColor: Colors.red, + ), + ); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Admin Dashboard'), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _loadDashboardData, + ), + IconButton( + icon: const Icon(Icons.notifications), + onPressed: () { + Navigator.pushNamed(context, '/notifications'); + }, + ), + ], + ), + body: + _isLoading + ? const Center(child: CircularProgressIndicator()) + : RefreshIndicator( + onRefresh: _loadDashboardData, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildWelcomeSection(), + const SizedBox(height: 24), + _buildStatsGrid(), + const SizedBox(height: 24), + _buildQuickActions(), + const SizedBox(height: 24), + _buildStatusChart(), + const SizedBox(height: 24), + _buildPendingGrievances(), + const SizedBox(height: 24), + _buildRecentActivity(), + ], + ), + ), + ), + ); + } + + Widget _buildWelcomeSection() { + final currentUser = GrievanceService.getCurrentUser(); + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + CircleAvatar( + radius: 30, + backgroundColor: Theme.of(context).primaryColor, + child: Text( + currentUser?.name.substring(0, 1).toUpperCase() ?? 'A', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Welcome back, ${currentUser?.name ?? 'Admin'}!', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 4), + Text( + 'Administrator Dashboard', + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: Colors.grey[600]), + ), + const SizedBox(height: 8), + Row( + children: [ + Icon(Icons.schedule, size: 16, color: Colors.grey[600]), + const SizedBox(width: 4), + Text( + 'Last updated: ${DateTime.now().toString().substring(0, 16)}', + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildStatsGrid() { + if (_analytics == null) return const SizedBox.shrink(); + + return GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 2, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + childAspectRatio: 1.5, + children: [ + StatCard( + title: 'Total Grievances', + value: _analytics!.totalGrievances.toString(), + icon: Icons.list_alt, + color: Colors.blue, + ), + StatCard( + title: 'Pending Review', + value: _analytics!.pendingCount.toString(), + icon: Icons.hourglass_empty, + color: Colors.orange, + isUrgent: _analytics!.pendingCount > 5, + ), + StatCard( + title: 'Resolved', + value: _analytics!.resolvedCount.toString(), + icon: Icons.check_circle, + color: Colors.green, + ), + StatCard( + title: 'Avg Resolution', + value: '${_analytics!.averageResolutionTime.toStringAsFixed(1)} days', + icon: Icons.timer, + color: Colors.purple, + isUrgent: _analytics!.averageResolutionTime > 7, + ), + ], + ); + } + + Widget _buildQuickActions() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Quick Actions', style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 16), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + QuickActionCard( + title: 'All Grievances', + subtitle: 'Manage all submissions', + icon: Icons.list, + color: Colors.blue, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AdminGrievancesScreen(), + ), + ); + }, + ), + const SizedBox(width: 16), + QuickActionCard( + title: 'Analytics', + subtitle: 'View detailed reports', + icon: Icons.analytics, + color: Colors.green, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AdminAnalyticsScreen(), + ), + ); + }, + ), + const SizedBox(width: 16), + QuickActionCard( + title: 'Categories', + subtitle: 'Manage categories', + icon: Icons.category, + color: Colors.purple, + onTap: () { + // Navigate to category management + }, + ), + const SizedBox(width: 16), + QuickActionCard( + title: 'Users', + subtitle: 'Manage users', + icon: Icons.people, + color: Colors.orange, + onTap: () { + // Navigate to user management + }, + ), + ], + ), + ), + ], + ); + } + + Widget _buildStatusChart() { + if (_analytics == null) return const SizedBox.shrink(); + + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Grievance Status Distribution', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + SizedBox( + height: 200, + child: PieChart( + PieChartData( + sections: [ + PieChartSectionData( + value: _analytics!.pendingCount.toDouble(), + title: 'Pending\n${_analytics!.pendingCount}', + color: Colors.orange, + radius: 80, + ), + PieChartSectionData( + value: _analytics!.underReviewCount.toDouble(), + title: 'Review\n${_analytics!.underReviewCount}', + color: Colors.blue, + radius: 80, + ), + PieChartSectionData( + value: _analytics!.inProgressCount.toDouble(), + title: 'Progress\n${_analytics!.inProgressCount}', + color: Colors.purple, + radius: 80, + ), + PieChartSectionData( + value: _analytics!.resolvedCount.toDouble(), + title: 'Resolved\n${_analytics!.resolvedCount}', + color: Colors.green, + radius: 80, + ), + ], + centerSpaceRadius: 40, + sectionsSpace: 2, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildPendingGrievances() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Urgent Pending Grievances', + style: Theme.of(context).textTheme.titleLarge, + ), + TextButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: + (context) => const AdminGrievancesScreen( + initialFilter: 'pending', + ), + ), + ); + }, + child: const Text('View All'), + ), + ], + ), + const SizedBox(height: 16), + if (_pendingGrievances.isEmpty) + Card( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Row( + children: [ + Icon(Icons.check_circle, color: Colors.green, size: 32), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'All caught up!', + style: Theme.of(context).textTheme.titleMedium, + ), + Text( + 'No pending grievances require immediate attention.', + style: TextStyle(color: Colors.grey[600]), + ), + ], + ), + ), + ], + ), + ), + ) + else + ...(_pendingGrievances.map( + (grievance) => GrievanceCard( + grievance: grievance, + onTap: () { + // Navigate to grievance detail for admin + }, + ), + )), + ], + ); + } + + Widget _buildRecentActivity() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Recent Grievances', + style: Theme.of(context).textTheme.titleLarge, + ), + TextButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AdminGrievancesScreen(), + ), + ); + }, + child: const Text('View All'), + ), + ], + ), + const SizedBox(height: 16), + if (_recentGrievances.isEmpty) + Card( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Center( + child: Column( + children: [ + Icon(Icons.inbox, size: 48, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + 'No recent grievances', + style: TextStyle(fontSize: 16, color: Colors.grey[600]), + ), + ], + ), + ), + ), + ) + else + ...(_recentGrievances.map( + (grievance) => GrievanceCard( + grievance: grievance, + onTap: () { + // Navigate to grievance detail for admin + }, + ), + )), + ], + ); + } +} diff --git a/lib/screens/admin/admin_grievances_screen.dart b/lib/screens/admin/admin_grievances_screen.dart new file mode 100644 index 0000000..d24a288 --- /dev/null +++ b/lib/screens/admin/admin_grievances_screen.dart @@ -0,0 +1,218 @@ +import 'package:flutter/material.dart'; +import '../../models/grievance.dart'; +import '../../services/grievance_service.dart'; +import '../../widgets/grievance_card.dart'; +import '../../widgets/common_widgets.dart'; + +class AdminGrievancesScreen extends StatefulWidget { + final String? initialFilter; + + const AdminGrievancesScreen({super.key, this.initialFilter}); + + @override + State createState() => _AdminGrievancesScreenState(); +} + +class _AdminGrievancesScreenState extends State { + List _allGrievances = []; + List _filteredGrievances = []; + String _searchQuery = ''; + String? _selectedCategory; + String? _selectedStatus; + String? _selectedPriority; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _selectedStatus = widget.initialFilter; + _loadGrievances(); + } + + Future _loadGrievances() async { + setState(() { + _isLoading = true; + }); + + try { + _allGrievances = GrievanceService.getAllGrievances(); + _allGrievances.sort((a, b) => b.submittedDate.compareTo(a.submittedDate)); + _applyFilters(); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error loading grievances: $e'), + backgroundColor: Colors.red, + ), + ); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + void _applyFilters() { + _filteredGrievances = + _allGrievances.where((grievance) { + // Search filter + if (_searchQuery.isNotEmpty) { + final query = _searchQuery.toLowerCase(); + if (!grievance.title.toLowerCase().contains(query) && + !grievance.description.toLowerCase().contains(query) && + !grievance.category.toLowerCase().contains(query) && + !grievance.id.toLowerCase().contains(query)) { + return false; + } + } + + // Category filter + if (_selectedCategory != null && + grievance.category != _selectedCategory) { + return false; + } + + // Status filter + if (_selectedStatus != null && + grievance.status.name != _selectedStatus) { + return false; + } + + // Priority filter + if (_selectedPriority != null && + grievance.priority.name != _selectedPriority) { + return false; + } + + return true; + }).toList(); + + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('All Grievances'), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _loadGrievances, + ), + ], + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: SearchAndFilterBar( + searchQuery: _searchQuery, + selectedCategory: _selectedCategory, + selectedStatus: _selectedStatus, + selectedPriority: _selectedPriority, + onSearchChanged: (query) { + _searchQuery = query; + _applyFilters(); + }, + onCategoryChanged: (category) { + _selectedCategory = category; + _applyFilters(); + }, + onStatusChanged: (status) { + _selectedStatus = status; + _applyFilters(); + }, + onPriorityChanged: (priority) { + _selectedPriority = priority; + _applyFilters(); + }, + onClearFilters: () { + _searchQuery = ''; + _selectedCategory = null; + _selectedStatus = null; + _selectedPriority = null; + _applyFilters(); + }, + ), + ), + Expanded( + child: + _isLoading + ? const Center(child: CircularProgressIndicator()) + : _buildGrievanceList(), + ), + ], + ), + ); + } + + Widget _buildGrievanceList() { + if (_filteredGrievances.isEmpty) { + return _buildEmptyState(); + } + + return RefreshIndicator( + onRefresh: _loadGrievances, + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + itemCount: _filteredGrievances.length, + itemBuilder: (context, index) { + final grievance = _filteredGrievances[index]; + return GrievanceCard( + grievance: grievance, + onTap: () { + // Navigate to admin grievance detail + }, + ); + }, + ), + ); + } + + Widget _buildEmptyState() { + String message; + String subtitle; + IconData icon; + + if (_searchQuery.isNotEmpty || + _selectedCategory != null || + _selectedStatus != null || + _selectedPriority != null) { + icon = Icons.search_off; + message = 'No matching grievances'; + subtitle = 'Try adjusting your search or filters'; + } else { + icon = Icons.inbox; + message = 'No grievances found'; + subtitle = 'No grievances have been submitted yet'; + } + + return Center( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + message, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + subtitle, + style: TextStyle(fontSize: 14, color: Colors.grey[500]), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/student/grievance_detail_screen.dart b/lib/screens/student/grievance_detail_screen.dart new file mode 100644 index 0000000..698c4fc --- /dev/null +++ b/lib/screens/student/grievance_detail_screen.dart @@ -0,0 +1,641 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../../models/grievance.dart'; +import '../../services/grievance_service.dart'; +import '../../widgets/common_widgets.dart'; +import '../../utils/constants.dart'; + +class GrievanceDetailScreen extends StatefulWidget { + final Grievance grievance; + + const GrievanceDetailScreen({super.key, required this.grievance}); + + @override + State createState() => _GrievanceDetailScreenState(); +} + +class _GrievanceDetailScreenState extends State { + late Grievance currentGrievance; + int? _userRating; + final _feedbackController = TextEditingController(); + bool _isSubmittingFeedback = false; + + @override + void initState() { + super.initState(); + currentGrievance = widget.grievance; + _userRating = currentGrievance.rating; + _feedbackController.text = currentGrievance.feedback ?? ''; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + 'Grievance #${currentGrievance.id.substring(currentGrievance.id.length - 6)}', + ), + actions: [ + IconButton(icon: const Icon(Icons.share), onPressed: _shareGrievance), + PopupMenuButton( + onSelected: _handleMenuAction, + itemBuilder: + (BuildContext context) => [ + const PopupMenuItem( + value: 'refresh', + child: Row( + children: [ + Icon(Icons.refresh), + SizedBox(width: 8), + Text('Refresh'), + ], + ), + ), + if (currentGrievance.status == GrievanceStatus.resolved && + currentGrievance.rating == null) + const PopupMenuItem( + value: 'rate', + child: Row( + children: [ + Icon(Icons.star), + SizedBox(width: 8), + Text('Rate & Feedback'), + ], + ), + ), + const PopupMenuItem( + value: 'report', + child: Row( + children: [ + Icon(Icons.flag), + SizedBox(width: 8), + Text('Report Issue'), + ], + ), + ), + ], + ), + ], + ), + body: RefreshIndicator( + onRefresh: _refreshGrievance, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildStatusCard(), + const SizedBox(height: 16), + _buildDetailsCard(), + const SizedBox(height: 16), + _buildDescriptionCard(), + const SizedBox(height: 16), + if (currentGrievance.attachments.isNotEmpty) + _buildAttachmentsCard(), + const SizedBox(height: 16), + _buildTimelineCard(), + const SizedBox(height: 16), + if (currentGrievance.status == GrievanceStatus.resolved) + _buildFeedbackCard(), + const SizedBox(height: 24), + ], + ), + ), + ), + ); + } + + Widget _buildStatusCard() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Utils.getStatusColor(currentGrievance.status), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Utils.getStatusIcon(currentGrievance.status), + color: Colors.white, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + Utils.getStatusDisplayName(currentGrievance.status), + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + Text( + _getStatusDescription(), + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Utils.getPriorityColor( + currentGrievance.priority, + ).withOpacity(0.1), + border: Border.all( + color: Utils.getPriorityColor(currentGrievance.priority), + ), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + Utils.getPriorityDisplayName(currentGrievance.priority), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Utils.getPriorityColor(currentGrievance.priority), + ), + ), + ), + ], + ), + if (currentGrievance.status == GrievanceStatus.resolved && + currentGrievance.resolvedDate != null) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green[50], + border: Border.all(color: Colors.green[200]!), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon(Icons.check_circle, color: Colors.green[600]), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Resolved on ${Utils.formatDate(currentGrievance.resolvedDate!)}', + style: TextStyle( + color: Colors.green[800], + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ], + ], + ), + ), + ); + } + + Widget _buildDetailsCard() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Grievance Details', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + _buildDetailRow('Title', currentGrievance.title), + _buildDetailRow('Category', currentGrievance.category), + _buildDetailRow( + 'Priority', + Utils.getPriorityDisplayName(currentGrievance.priority), + ), + _buildDetailRow('Student ID', currentGrievance.studentId), + _buildDetailRow('Department', currentGrievance.department), + _buildDetailRow('Year', currentGrievance.year), + _buildDetailRow( + 'Submitted', + Utils.formatDateTime(currentGrievance.submittedDate), + ), + if (currentGrievance.lastUpdated != null) + _buildDetailRow( + 'Last Updated', + Utils.formatDateTime(currentGrievance.lastUpdated!), + ), + if (currentGrievance.assignedTo != null) + _buildDetailRow('Assigned To', currentGrievance.assignedTo!), + ], + ), + ), + ); + } + + Widget _buildDescriptionCard() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Description', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey[200]!), + ), + child: Text( + currentGrievance.description, + style: const TextStyle(fontSize: 16, height: 1.5), + ), + ), + ], + ), + ), + ); + } + + Widget _buildAttachmentsCard() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Attachments', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + ...currentGrievance.attachments.map((attachment) { + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: Icon( + _getFileIcon(attachment), + color: Colors.blue[600], + ), + title: Text(attachment.split('/').last), + subtitle: Text('Attachment'), + trailing: IconButton( + icon: const Icon(Icons.download), + onPressed: () => _downloadAttachment(attachment), + ), + ), + ); + }).toList(), + ], + ), + ), + ); + } + + Widget _buildTimelineCard() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Status Timeline', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + StatusTimeline(statusHistory: currentGrievance.statusHistory), + ], + ), + ), + ); + } + + Widget _buildFeedbackCard() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Rate Your Experience', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + if (currentGrievance.rating != null) ...[ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue[200]!), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text( + 'Your Rating: ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ...List.generate(5, (index) { + return Icon( + index < currentGrievance.rating! + ? Icons.star + : Icons.star_border, + color: Colors.amber, + size: 20, + ); + }), + const SizedBox(width: 8), + Text('(${currentGrievance.rating}/5)'), + ], + ), + if (currentGrievance.feedback != null && + currentGrievance.feedback!.isNotEmpty) ...[ + const SizedBox(height: 8), + const Text( + 'Your Feedback:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 4), + Text(currentGrievance.feedback!), + ], + ], + ), + ), + ] else ...[ + const Text( + 'How satisfied are you with the resolution of your grievance?', + style: TextStyle(fontSize: 16), + ), + const SizedBox(height: 16), + Row( + children: List.generate(5, (index) { + return GestureDetector( + onTap: () { + setState(() { + _userRating = index + 1; + }); + }, + child: Icon( + index < (_userRating ?? 0) + ? Icons.star + : Icons.star_border, + color: Colors.amber, + size: 32, + ), + ); + }), + ), + const SizedBox(height: 16), + TextField( + controller: _feedbackController, + decoration: const InputDecoration( + labelText: 'Additional Feedback (Optional)', + hintText: 'Share your experience...', + border: OutlineInputBorder(), + ), + maxLines: 3, + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: + _isSubmittingFeedback || _userRating == null + ? null + : _submitFeedback, + icon: + _isSubmittingFeedback + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.send), + label: Text( + _isSubmittingFeedback ? 'Submitting...' : 'Submit Feedback', + ), + ), + ), + ], + ], + ), + ), + ); + } + + Widget _buildDetailRow(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 120, + child: Text( + '$label:', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.grey[700], + ), + ), + ), + Expanded(child: Text(value, style: const TextStyle(fontSize: 16))), + ], + ), + ); + } + + String _getStatusDescription() { + switch (currentGrievance.status) { + case GrievanceStatus.pending: + return 'Your grievance is waiting to be reviewed'; + case GrievanceStatus.underReview: + return 'Your grievance is being reviewed by the relevant department'; + case GrievanceStatus.inProgress: + return 'Work is in progress to resolve your grievance'; + case GrievanceStatus.resolved: + return 'Your grievance has been resolved'; + case GrievanceStatus.closed: + return 'This grievance has been closed'; + } + } + + IconData _getFileIcon(String filePath) { + final extension = Utils.getFileExtension(filePath); + switch (extension) { + case 'pdf': + return Icons.picture_as_pdf; + case 'jpg': + case 'jpeg': + case 'png': + return Icons.image; + case 'doc': + case 'docx': + return Icons.description; + default: + return Icons.attach_file; + } + } + + Future _refreshGrievance() async { + final updatedGrievance = GrievanceService.getGrievanceById( + currentGrievance.id, + ); + if (updatedGrievance != null) { + setState(() { + currentGrievance = updatedGrievance; + }); + } + } + + Future _submitFeedback() async { + if (_userRating == null) return; + + setState(() { + _isSubmittingFeedback = true; + }); + + try { + await GrievanceService.rateGrievance( + currentGrievance.id, + _userRating!, + _feedbackController.text.trim().isEmpty + ? null + : _feedbackController.text.trim(), + ); + + setState(() { + currentGrievance = currentGrievance.copyWith( + rating: _userRating, + feedback: + _feedbackController.text.trim().isEmpty + ? null + : _feedbackController.text.trim(), + ); + }); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Thank you for your feedback!'), + backgroundColor: Colors.green, + ), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error submitting feedback: $e'), + backgroundColor: Colors.red, + ), + ); + } finally { + setState(() { + _isSubmittingFeedback = false; + }); + } + } + + void _handleMenuAction(String action) { + switch (action) { + case 'refresh': + _refreshGrievance(); + break; + case 'rate': + // Scroll to feedback card + break; + case 'report': + _showReportDialog(); + break; + } + } + + void _shareGrievance() { + // TODO: Implement actual sharing functionality + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Share functionality will be implemented')), + ); + } + + void _downloadAttachment(String attachment) { + // TODO: Implement download functionality + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Downloading ${attachment.split('/').last}...')), + ); + } + + void _showReportDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Report Issue'), + content: const Text( + 'If you\'re experiencing any issues with this grievance or the system, please contact the helpdesk.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + _contactHelpdesk(); + }, + child: const Text('Contact Helpdesk'), + ), + ], + ); + }, + ); + } + + void _contactHelpdesk() async { + const email = 'helpdesk@college.edu'; + const subject = 'Grievance System Issue'; + final body = + 'I need help with Grievance #${currentGrievance.id.substring(currentGrievance.id.length - 6)}'; + + final Uri emailUri = Uri( + scheme: 'mailto', + path: email, + queryParameters: {'subject': subject, 'body': body}, + ); + + try { + await launchUrl(emailUri); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Could not open email client'), + backgroundColor: Colors.red, + ), + ); + } + } + + @override + void dispose() { + _feedbackController.dispose(); + super.dispose(); + } +} diff --git a/lib/screens/student/home_screen.dart b/lib/screens/student/home_screen.dart new file mode 100644 index 0000000..4ac6a4a --- /dev/null +++ b/lib/screens/student/home_screen.dart @@ -0,0 +1,366 @@ +import 'package:flutter/material.dart'; +import '../../models/user.dart'; +import '../../models/grievance.dart'; +import '../../services/grievance_service.dart'; +import '../../widgets/stat_card.dart'; +import '../../widgets/grievance_card.dart'; +import '../../widgets/quick_action_card.dart'; +import 'submit_grievance_screen.dart'; +import 'grievance_detail_screen.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + User? currentUser; + late Future _initFuture; + + @override + void initState() { + super.initState(); + _initFuture = _initializeData(); + } + + Future _initializeData() async { + await GrievanceService.initialize(); + currentUser = GrievanceService.getCurrentUser(); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('College Grievance System'), + actions: [ + IconButton( + icon: const Icon(Icons.notifications), + onPressed: () { + // TODO: Navigate to notifications + }, + ), + ], + ), + body: FutureBuilder( + future: _initFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + return RefreshIndicator( + onRefresh: _initializeData, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildWelcomeCard(), + const SizedBox(height: 24), + _buildQuickStats(), + const SizedBox(height: 24), + _buildQuickActions(), + const SizedBox(height: 24), + _buildRecentGrievances(), + ], + ), + ), + ); + }, + ), + ); + } + + Widget _buildWelcomeCard() { + return Card( + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.blue[400]!, Colors.blue[600]!], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Welcome back, ${currentUser?.name ?? 'Student'}!', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 8), + const Text( + 'Submit your problems and track their resolution', + style: TextStyle(fontSize: 16, color: Colors.white70), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SubmitGrievanceScreen(), + ), + ); + }, + icon: const Icon(Icons.add), + label: const Text('Submit New Grievance'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.blue[600], + ), + ), + ], + ), + ), + ); + } + + Widget _buildQuickStats() { + if (currentUser == null) return const SizedBox.shrink(); + + final userGrievances = GrievanceService.getGrievancesByStudent( + currentUser!.studentId!, + ); + final resolvedCount = + userGrievances + .where((g) => g.status == GrievanceStatus.resolved) + .length; + final pendingCount = + userGrievances + .where( + (g) => + g.status == GrievanceStatus.pending || + g.status == GrievanceStatus.underReview, + ) + .length; + final inProgressCount = + userGrievances + .where((g) => g.status == GrievanceStatus.inProgress) + .length; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Your Grievances', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: StatCard( + title: 'Total', + value: userGrievances.length.toString(), + icon: Icons.list, + color: Colors.blue, + ), + ), + const SizedBox(width: 8), + Expanded( + child: StatCard( + title: 'Pending', + value: pendingCount.toString(), + icon: Icons.hourglass_empty, + color: Colors.orange, + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: StatCard( + title: 'In Progress', + value: inProgressCount.toString(), + icon: Icons.work, + color: Colors.amber, + ), + ), + const SizedBox(width: 8), + Expanded( + child: StatCard( + title: 'Resolved', + value: resolvedCount.toString(), + icon: Icons.check_circle, + color: Colors.green, + ), + ), + ], + ), + ], + ); + } + + Widget _buildQuickActions() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Quick Actions', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + GridView.count( + crossAxisCount: 2, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisSpacing: 8, + mainAxisSpacing: 8, + childAspectRatio: 1.2, + children: [ + QuickActionCard( + title: 'Academic Issues', + subtitle: 'Classroom & Course Problems', + icon: Icons.school, + color: Colors.orange, + onTap: () => _navigateToSubmitGrievance('Academic Issues'), + ), + QuickActionCard( + title: 'Hostel Problems', + subtitle: 'Room & Facility Issues', + icon: Icons.home, + color: Colors.green, + onTap: () => _navigateToSubmitGrievance('Hostel Problems'), + ), + QuickActionCard( + title: 'Library Issues', + subtitle: 'Books & Study Space', + icon: Icons.library_books, + color: Colors.purple, + onTap: () => _navigateToSubmitGrievance('Library Issues'), + ), + QuickActionCard( + title: 'Canteen Complaints', + subtitle: 'Food & Service Quality', + icon: Icons.restaurant, + color: Colors.red, + onTap: () => _navigateToSubmitGrievance('Canteen Complaints'), + ), + QuickActionCard( + title: 'Transport Issues', + subtitle: 'Bus & Travel Problems', + icon: Icons.directions_bus, + color: Colors.teal, + onTap: () => _navigateToSubmitGrievance('Transport Issues'), + ), + QuickActionCard( + title: 'Other Issues', + subtitle: 'General Problems', + icon: Icons.more_horiz, + color: Colors.grey, + onTap: () => _navigateToSubmitGrievance('Other'), + ), + ], + ), + ], + ); + } + + Widget _buildRecentGrievances() { + if (currentUser == null) return const SizedBox.shrink(); + + final userGrievances = GrievanceService.getGrievancesByStudent( + currentUser!.studentId!, + ); + final recentGrievances = userGrievances.take(3).toList(); + + if (recentGrievances.isEmpty) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Recent Grievances', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Card( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + children: [ + Icon(Icons.inbox, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + 'No grievances yet', + style: TextStyle(fontSize: 18, color: Colors.grey[600]), + ), + const SizedBox(height: 8), + Text( + 'Submit your first grievance to get started', + style: TextStyle(fontSize: 14, color: Colors.grey[500]), + ), + ], + ), + ), + ), + ], + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text( + 'Recent Grievances', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const Spacer(), + TextButton( + onPressed: () { + // TODO: Navigate to all grievances + }, + child: const Text('View All'), + ), + ], + ), + const SizedBox(height: 16), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: recentGrievances.length, + itemBuilder: (context, index) { + final grievance = recentGrievances[index]; + return GrievanceCard( + grievance: grievance, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: + (context) => + GrievanceDetailScreen(grievance: grievance), + ), + ); + }, + ); + }, + ), + ], + ); + } + + void _navigateToSubmitGrievance(String category) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SubmitGrievanceScreen(category: category), + ), + ); + } +} diff --git a/lib/screens/student/my_grievances_screen.dart b/lib/screens/student/my_grievances_screen.dart new file mode 100644 index 0000000..c6f5694 --- /dev/null +++ b/lib/screens/student/my_grievances_screen.dart @@ -0,0 +1,312 @@ +import 'package:flutter/material.dart'; +import '../../models/grievance.dart'; +import '../../services/grievance_service.dart'; +import '../../widgets/grievance_card.dart'; +import '../../widgets/common_widgets.dart'; +import 'grievance_detail_screen.dart'; + +class MyGrievancesScreen extends StatefulWidget { + const MyGrievancesScreen({super.key}); + + @override + State createState() => _MyGrievancesScreenState(); +} + +class _MyGrievancesScreenState extends State + with TickerProviderStateMixin { + List _allGrievances = []; + List _filteredGrievances = []; + String _searchQuery = ''; + String? _selectedCategory; + String? _selectedStatus; + String? _selectedPriority; + late TabController _tabController; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 5, vsync: this); + _loadGrievances(); + } + + Future _loadGrievances() async { + setState(() { + _isLoading = true; + }); + + try { + final currentUser = GrievanceService.getCurrentUser(); + if (currentUser != null) { + _allGrievances = GrievanceService.getGrievancesByStudent( + currentUser.studentId!, + ); + _allGrievances.sort( + (a, b) => b.submittedDate.compareTo(a.submittedDate), + ); + _applyFilters(); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error loading grievances: $e'), + backgroundColor: Colors.red, + ), + ); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + void _applyFilters() { + _filteredGrievances = + _allGrievances.where((grievance) { + // Search filter + if (_searchQuery.isNotEmpty) { + final query = _searchQuery.toLowerCase(); + if (!grievance.title.toLowerCase().contains(query) && + !grievance.description.toLowerCase().contains(query) && + !grievance.category.toLowerCase().contains(query) && + !grievance.id.toLowerCase().contains(query)) { + return false; + } + } + + // Category filter + if (_selectedCategory != null && + grievance.category != _selectedCategory) { + return false; + } + + // Status filter + if (_selectedStatus != null && + grievance.status.name != _selectedStatus) { + return false; + } + + // Priority filter + if (_selectedPriority != null && + grievance.priority.name != _selectedPriority) { + return false; + } + + return true; + }).toList(); + + setState(() {}); + } + + List _getGrievancesByStatus(GrievanceStatus status) { + return _filteredGrievances.where((g) => g.status == status).toList(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('My Grievances'), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _loadGrievances, + ), + ], + bottom: TabBar( + controller: _tabController, + isScrollable: true, + tabs: [ + Tab( + text: 'All (${_filteredGrievances.length})', + icon: const Icon(Icons.list, size: 16), + ), + Tab( + text: + 'Pending (${_getGrievancesByStatus(GrievanceStatus.pending).length})', + icon: const Icon(Icons.hourglass_empty, size: 16), + ), + Tab( + text: + 'Review (${_getGrievancesByStatus(GrievanceStatus.underReview).length})', + icon: const Icon(Icons.visibility, size: 16), + ), + Tab( + text: + 'Progress (${_getGrievancesByStatus(GrievanceStatus.inProgress).length})', + icon: const Icon(Icons.work, size: 16), + ), + Tab( + text: + 'Resolved (${_getGrievancesByStatus(GrievanceStatus.resolved).length})', + icon: const Icon(Icons.check_circle, size: 16), + ), + ], + ), + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: SearchAndFilterBar( + searchQuery: _searchQuery, + selectedCategory: _selectedCategory, + selectedStatus: _selectedStatus, + selectedPriority: _selectedPriority, + onSearchChanged: (query) { + _searchQuery = query; + _applyFilters(); + }, + onCategoryChanged: (category) { + _selectedCategory = category; + _applyFilters(); + }, + onStatusChanged: (status) { + _selectedStatus = status; + _applyFilters(); + }, + onPriorityChanged: (priority) { + _selectedPriority = priority; + _applyFilters(); + }, + onClearFilters: () { + _searchQuery = ''; + _selectedCategory = null; + _selectedStatus = null; + _selectedPriority = null; + _applyFilters(); + }, + ), + ), + Expanded( + child: + _isLoading + ? const Center(child: CircularProgressIndicator()) + : TabBarView( + controller: _tabController, + children: [ + _buildGrievanceList(_filteredGrievances), + _buildGrievanceList( + _getGrievancesByStatus(GrievanceStatus.pending), + ), + _buildGrievanceList( + _getGrievancesByStatus(GrievanceStatus.underReview), + ), + _buildGrievanceList( + _getGrievancesByStatus(GrievanceStatus.inProgress), + ), + _buildGrievanceList( + _getGrievancesByStatus(GrievanceStatus.resolved), + ), + ], + ), + ), + ], + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () { + Navigator.pushNamed(context, '/submit-grievance').then((_) { + _loadGrievances(); // Refresh after submitting + }); + }, + icon: const Icon(Icons.add), + label: const Text('New Grievance'), + ), + ); + } + + Widget _buildGrievanceList(List grievances) { + if (grievances.isEmpty) { + return _buildEmptyState(); + } + + return RefreshIndicator( + onRefresh: _loadGrievances, + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + itemCount: grievances.length, + itemBuilder: (context, index) { + final grievance = grievances[index]; + return GrievanceCard( + grievance: grievance, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: + (context) => GrievanceDetailScreen(grievance: grievance), + ), + ).then((_) { + _loadGrievances(); // Refresh after viewing details + }); + }, + ); + }, + ), + ); + } + + Widget _buildEmptyState() { + String message; + String subtitle; + IconData icon; + + if (_searchQuery.isNotEmpty || + _selectedCategory != null || + _selectedStatus != null || + _selectedPriority != null) { + icon = Icons.search_off; + message = 'No matching grievances'; + subtitle = 'Try adjusting your search or filters'; + } else { + icon = Icons.inbox; + message = 'No grievances yet'; + subtitle = 'Submit your first grievance to get started'; + } + + return Center( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + message, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + subtitle, + style: TextStyle(fontSize: 14, color: Colors.grey[500]), + textAlign: TextAlign.center, + ), + if (message == 'No grievances yet') ...[ + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: () { + Navigator.pushNamed(context, '/submit-grievance').then((_) { + _loadGrievances(); + }); + }, + icon: const Icon(Icons.add), + label: const Text('Submit Grievance'), + ), + ], + ], + ), + ), + ); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } +} diff --git a/lib/screens/student/profile_screen.dart b/lib/screens/student/profile_screen.dart new file mode 100644 index 0000000..e712185 --- /dev/null +++ b/lib/screens/student/profile_screen.dart @@ -0,0 +1,252 @@ +import 'package:flutter/material.dart'; +import '../../models/user.dart'; +import '../../services/grievance_service.dart'; +import '../../services/theme_provider.dart'; +import 'package:provider/provider.dart'; + +class ProfileScreen extends StatefulWidget { + const ProfileScreen({super.key}); + + @override + State createState() => _ProfileScreenState(); +} + +class _ProfileScreenState extends State { + User? currentUser; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadUserData(); + } + + Future _loadUserData() async { + setState(() { + _isLoading = true; + }); + + try { + await GrievanceService.initialize(); + currentUser = GrievanceService.getCurrentUser(); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } + + final themeProvider = Provider.of(context); + final userGrievances = + currentUser != null + ? GrievanceService.getGrievancesByStudent(currentUser!.studentId!) + : []; + + final resolvedCount = + userGrievances.where((g) => g.status.name == 'resolved').length; + final pendingCount = + userGrievances + .where( + (g) => + g.status.name == 'pending' || g.status.name == 'underReview', + ) + .length; + final inProgressCount = + userGrievances.where((g) => g.status.name == 'inProgress').length; + + return Scaffold( + appBar: AppBar( + title: const Text('Profile'), + actions: [ + IconButton( + icon: Icon( + themeProvider.isDarkMode ? Icons.light_mode : Icons.dark_mode, + ), + onPressed: () { + themeProvider.toggleTheme(); + }, + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + CircleAvatar( + radius: 50, + backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1), + child: Text( + currentUser?.name.substring(0, 1).toUpperCase() ?? 'U', + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + ), + ), + ), + const SizedBox(height: 16), + Text( + currentUser?.name ?? 'User', + style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + Text( + 'Student ID: ${currentUser?.studentId ?? 'N/A'}', + style: TextStyle(fontSize: 16, color: Colors.grey[600]), + ), + const SizedBox(height: 32), + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Personal Information', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + _buildInfoRow('Email:', currentUser?.email ?? 'N/A'), + _buildInfoRow( + 'Department:', + currentUser?.department ?? 'N/A', + ), + _buildInfoRow('Year:', currentUser?.year ?? 'N/A'), + _buildInfoRow('Role:', currentUser?.role.name ?? 'Student'), + ], + ), + ), + ), + const SizedBox(height: 16), + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Grievance Statistics', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + _buildInfoRow( + 'Total Submitted:', + userGrievances.length.toString(), + ), + _buildInfoRow('Resolved:', resolvedCount.toString()), + _buildInfoRow('In Progress:', inProgressCount.toString()), + _buildInfoRow('Pending:', pendingCount.toString()), + ], + ), + ), + ), + const SizedBox(height: 16), + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Settings', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + ListTile( + leading: Icon( + themeProvider.isDarkMode + ? Icons.dark_mode + : Icons.light_mode, + ), + title: const Text('Dark Mode'), + trailing: Switch( + value: themeProvider.isDarkMode, + onChanged: (value) { + themeProvider.toggleTheme(); + }, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 32), + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: () { + _showEditProfileDialog(context); + }, + child: const Text( + 'Edit Profile', + style: TextStyle(fontSize: 16), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildInfoRow(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 120, + child: Text( + label, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.grey[600], + ), + ), + ), + Expanded(child: Text(value)), + ], + ), + ); + } + + void _showEditProfileDialog(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Edit Profile'), + content: const Text( + 'Profile editing functionality will be implemented in a future update.', + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('OK'), + ), + ], + ); + }, + ); + } +} diff --git a/lib/screens/student/submit_grievance_screen.dart b/lib/screens/student/submit_grievance_screen.dart new file mode 100644 index 0000000..0edaf8e --- /dev/null +++ b/lib/screens/student/submit_grievance_screen.dart @@ -0,0 +1,808 @@ +import 'package:flutter/material.dart'; +import 'package:file_picker/file_picker.dart'; +import '../../models/grievance.dart'; +import '../../services/grievance_service.dart'; +import '../../utils/constants.dart'; + +class SubmitGrievanceScreen extends StatefulWidget { + final String? category; + + const SubmitGrievanceScreen({super.key, this.category}); + + @override + State createState() => _SubmitGrievanceScreenState(); +} + +class _SubmitGrievanceScreenState extends State { + final _formKey = GlobalKey(); + final _titleController = TextEditingController(); + final _nameController = TextEditingController(); + final _idController = TextEditingController(); + final _emailController = TextEditingController(); + final _phoneController = TextEditingController(); + final _descriptionController = TextEditingController(); + + String? _selectedCategory; + String? _selectedDepartment; + String? _selectedYear; + String? _selectedPriority; + List _attachments = []; + bool _isSubmitting = false; + bool _agreedToTerms = false; + + @override + void initState() { + super.initState(); + if (widget.category != null) { + _selectedCategory = widget.category; + } + _prefillUserData(); + } + + void _prefillUserData() { + final currentUser = GrievanceService.getCurrentUser(); + if (currentUser != null) { + _nameController.text = currentUser.name; + _idController.text = currentUser.studentId ?? ''; + _emailController.text = currentUser.email; + _phoneController.text = currentUser.phone; + _selectedDepartment = currentUser.department; + _selectedYear = currentUser.year; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Submit Grievance'), + actions: [ + TextButton( + onPressed: _isSubmitting ? null : () => _showHelpDialog(), + child: const Text('Help', style: TextStyle(color: Colors.white)), + ), + ], + ), + body: Form( + key: _formKey, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildProgressIndicator(), + const SizedBox(height: 24), + _buildStudentInformationSection(), + const SizedBox(height: 24), + _buildGrievanceDetailsSection(), + const SizedBox(height: 24), + _buildAttachmentsSection(), + const SizedBox(height: 24), + _buildTermsAndConditions(), + const SizedBox(height: 32), + _buildSubmitButton(), + ], + ), + ), + ), + ); + } + + Widget _buildProgressIndicator() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Submission Progress', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + LinearProgressIndicator( + value: _calculateProgress(), + backgroundColor: Colors.grey[300], + ), + const SizedBox(height: 8), + Text( + '${(_calculateProgress() * 100).round()}% Complete', + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + ], + ), + ), + ); + } + + double _calculateProgress() { + int completed = 0; + int total = 8; + + if (_nameController.text.isNotEmpty) completed++; + if (_idController.text.isNotEmpty) completed++; + if (_emailController.text.isNotEmpty) completed++; + if (_phoneController.text.isNotEmpty) completed++; + if (_selectedDepartment != null) completed++; + if (_selectedYear != null) completed++; + if (_selectedCategory != null) completed++; + if (_descriptionController.text.isNotEmpty) completed++; + + return completed / total; + } + + Widget _buildStudentInformationSection() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.person, color: Colors.blue[600]), + const SizedBox(width: 8), + const Text( + 'Student Information', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ], + ), + const SizedBox(height: 16), + TextFormField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Full Name', + prefixIcon: Icon(Icons.person), + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your name'; + } + return null; + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _idController, + decoration: const InputDecoration( + labelText: 'Student ID', + prefixIcon: Icon(Icons.badge), + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your student ID'; + } + return null; + }, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: TextFormField( + controller: _emailController, + decoration: const InputDecoration( + labelText: 'Email Address', + prefixIcon: Icon(Icons.email), + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.emailAddress, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your email'; + } + if (!Utils.isValidEmail(value)) { + return 'Please enter a valid email'; + } + return null; + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: TextFormField( + controller: _phoneController, + decoration: const InputDecoration( + labelText: 'Phone Number', + prefixIcon: Icon(Icons.phone), + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.phone, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your phone number'; + } + if (!Utils.isValidPhone(value)) { + return 'Please enter a valid phone number'; + } + return null; + }, + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: DropdownButtonFormField( + value: _selectedDepartment, + decoration: const InputDecoration( + labelText: 'Department', + prefixIcon: Icon(Icons.account_balance), + border: OutlineInputBorder(), + ), + items: + Constants.departments.map((String department) { + return DropdownMenuItem( + value: department, + child: Text(department), + ); + }).toList(), + onChanged: (String? newValue) { + setState(() { + _selectedDepartment = newValue; + }); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please select your department'; + } + return null; + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: DropdownButtonFormField( + value: _selectedYear, + decoration: const InputDecoration( + labelText: 'Year of Study', + prefixIcon: Icon(Icons.class_), + border: OutlineInputBorder(), + ), + items: + Constants.yearsOfStudy.map((String year) { + return DropdownMenuItem( + value: year, + child: Text(year), + ); + }).toList(), + onChanged: (String? newValue) { + setState(() { + _selectedYear = newValue; + }); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please select your year of study'; + } + return null; + }, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildGrievanceDetailsSection() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.report_problem, color: Colors.blue[600]), + const SizedBox(width: 8), + const Text( + 'Grievance Details', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ], + ), + const SizedBox(height: 16), + TextFormField( + controller: _titleController, + decoration: const InputDecoration( + labelText: 'Grievance Title', + prefixIcon: Icon(Icons.title), + border: OutlineInputBorder(), + hintText: 'Brief summary of your issue', + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a title for your grievance'; + } + if (value.length < 10) { + return 'Title should be at least 10 characters long'; + } + return null; + }, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: DropdownButtonFormField( + value: _selectedCategory, + decoration: const InputDecoration( + labelText: 'Category', + prefixIcon: Icon(Icons.category), + border: OutlineInputBorder(), + ), + items: + Constants.grievanceCategories.map((String category) { + return DropdownMenuItem( + value: category, + child: Text(category), + ); + }).toList(), + onChanged: (String? newValue) { + setState(() { + _selectedCategory = newValue; + }); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please select a category'; + } + return null; + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: DropdownButtonFormField( + value: _selectedPriority, + decoration: const InputDecoration( + labelText: 'Priority Level', + prefixIcon: Icon(Icons.priority_high), + border: OutlineInputBorder(), + ), + items: + Constants.priorityLevels.map((String priority) { + return DropdownMenuItem( + value: priority, + child: Row( + children: [ + Icon( + Utils.getPriorityIcon( + _getPriorityEnum(priority), + ), + size: 16, + color: Utils.getPriorityColor( + _getPriorityEnum(priority), + ), + ), + const SizedBox(width: 8), + Text(priority), + ], + ), + ); + }).toList(), + onChanged: (String? newValue) { + setState(() { + _selectedPriority = newValue; + }); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please select priority level'; + } + return null; + }, + ), + ), + ], + ), + const SizedBox(height: 16), + TextFormField( + controller: _descriptionController, + decoration: const InputDecoration( + labelText: 'Detailed Description', + prefixIcon: Icon(Icons.description), + border: OutlineInputBorder(), + hintText: 'Describe your problem in detail...', + alignLabelWithHint: true, + ), + maxLines: 5, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please describe your problem'; + } + if (value.length < 20) { + return 'Description should be at least 20 characters long'; + } + return null; + }, + ), + ], + ), + ), + ); + } + + Widget _buildAttachmentsSection() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.attach_file, color: Colors.blue[600]), + const SizedBox(width: 8), + const Text( + 'Attachments (Optional)', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ], + ), + const SizedBox(height: 8), + Text( + 'Attach relevant documents, images, or screenshots to support your grievance.', + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + const SizedBox(height: 16), + OutlinedButton.icon( + onPressed: _pickFiles, + icon: const Icon(Icons.cloud_upload), + label: const Text('Choose Files'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 24, + ), + ), + ), + if (_attachments.isNotEmpty) ...[ + const SizedBox(height: 16), + ..._attachments.asMap().entries.map((entry) { + final index = entry.key; + final file = entry.value; + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: Icon(_getFileIcon(file), color: Colors.blue[600]), + title: Text(file.split('/').last), + subtitle: Text('File ${index + 1}'), + trailing: IconButton( + icon: const Icon(Icons.delete, color: Colors.red), + onPressed: () { + setState(() { + _attachments.removeAt(index); + }); + }, + ), + ), + ); + }).toList(), + ], + ], + ), + ), + ); + } + + Widget _buildTermsAndConditions() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.gavel, color: Colors.blue[600]), + const SizedBox(width: 8), + const Text( + 'Terms & Conditions', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ], + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey[300]!), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'By submitting this grievance, you agree to:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text('• Provide accurate and truthful information'), + const Text( + '• Allow the college to investigate your complaint', + ), + const Text('• Cooperate with the resolution process'), + const Text('• Accept the college\'s final decision'), + const SizedBox(height: 8), + Text( + 'Your privacy will be protected and information will only be shared with relevant authorities.', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + fontStyle: FontStyle.italic, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + CheckboxListTile( + value: _agreedToTerms, + onChanged: (value) { + setState(() { + _agreedToTerms = value ?? false; + }); + }, + title: const Text('I agree to the terms and conditions'), + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + ), + ], + ), + ), + ); + } + + Widget _buildSubmitButton() { + return SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton.icon( + onPressed: _isSubmitting || !_agreedToTerms ? null : _submitGrievance, + icon: + _isSubmitting + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : const Icon(Icons.send), + label: Text(_isSubmitting ? 'Submitting...' : 'Submit Grievance'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue[600], + foregroundColor: Colors.white, + ), + ), + ); + } + + Future _pickFiles() async { + try { + FilePickerResult? result = await FilePicker.platform.pickFiles( + allowMultiple: true, + type: FileType.custom, + allowedExtensions: Constants.allowedFileTypes, + ); + + if (result != null) { + final newFiles = + result.paths.where((path) => path != null).cast(); + setState(() { + _attachments.addAll(newFiles); + }); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error picking files: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + + IconData _getFileIcon(String filePath) { + final extension = Utils.getFileExtension(filePath); + switch (extension) { + case 'pdf': + return Icons.picture_as_pdf; + case 'jpg': + case 'jpeg': + case 'png': + return Icons.image; + case 'doc': + case 'docx': + return Icons.description; + default: + return Icons.attach_file; + } + } + + Priority _getPriorityEnum(String priority) { + switch (priority) { + case 'Low': + return Priority.low; + case 'Medium': + return Priority.medium; + case 'High': + return Priority.high; + case 'Urgent': + return Priority.urgent; + default: + return Priority.medium; + } + } + + Future _submitGrievance() async { + if (!_formKey.currentState!.validate()) { + return; + } + + if (!_agreedToTerms) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please agree to the terms and conditions'), + backgroundColor: Colors.red, + ), + ); + return; + } + + setState(() { + _isSubmitting = true; + }); + + try { + final grievance = Grievance( + id: '', // Will be generated by service + title: _titleController.text.trim(), + description: _descriptionController.text.trim(), + category: _selectedCategory!, + priority: _getPriorityEnum(_selectedPriority!), + status: GrievanceStatus.pending, + studentId: _idController.text.trim(), + studentName: _nameController.text.trim(), + studentEmail: _emailController.text.trim(), + department: _selectedDepartment!, + year: _selectedYear!, + submittedDate: DateTime.now(), + attachments: _attachments, + ); + + final grievanceId = await GrievanceService.submitGrievance(grievance); + + // Show success dialog + if (mounted) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + icon: const Icon( + Icons.check_circle, + color: Colors.green, + size: 48, + ), + title: const Text('Success!'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Your grievance has been submitted successfully.'), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8), + ), + child: Text( + 'Grievance ID: ${grievanceId.substring(grievanceId.length - 6)}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontFamily: 'monospace', + ), + ), + ), + const SizedBox(height: 8), + const Text( + 'You will receive email updates about the status of your grievance.', + style: TextStyle(fontSize: 12), + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); // Close dialog + Navigator.of(context).pop(); // Close submission screen + }, + child: const Text('OK'), + ), + ], + ); + }, + ); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error submitting grievance: $e'), + backgroundColor: Colors.red, + ), + ); + } finally { + setState(() { + _isSubmitting = false; + }); + } + } + + void _showHelpDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Help & Guidelines'), + content: const SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Tips for submitting an effective grievance:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + SizedBox(height: 8), + Text('• Be specific and clear in your description'), + Text('• Provide relevant dates and locations'), + Text('• Attach supporting documents if available'), + Text('• Choose the correct category and priority'), + Text('• Provide accurate contact information'), + SizedBox(height: 16), + Text( + 'Processing Time:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + SizedBox(height: 8), + Text('• Low Priority: 5-7 working days'), + Text('• Medium Priority: 3-5 working days'), + Text('• High Priority: 1-3 working days'), + Text('• Urgent: Within 24 hours'), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Got it'), + ), + ], + ); + }, + ); + } + + @override + void dispose() { + _titleController.dispose(); + _nameController.dispose(); + _idController.dispose(); + _emailController.dispose(); + _phoneController.dispose(); + _descriptionController.dispose(); + super.dispose(); + } +} diff --git a/lib/services/analytics_service.dart b/lib/services/analytics_service.dart new file mode 100644 index 0000000..846048a --- /dev/null +++ b/lib/services/analytics_service.dart @@ -0,0 +1,281 @@ +import '../models/analytics.dart'; +import '../models/grievance.dart'; +import 'grievance_service.dart'; + +class AnalyticsService { + static Future getOverallAnalytics() async { + final grievances = GrievanceService.getAllGrievances(); + + // Count by status + final pendingCount = + grievances.where((g) => g.status == GrievanceStatus.pending).length; + final underReviewCount = + grievances.where((g) => g.status == GrievanceStatus.underReview).length; + final inProgressCount = + grievances.where((g) => g.status == GrievanceStatus.inProgress).length; + final resolvedCount = + grievances.where((g) => g.status == GrievanceStatus.resolved).length; + final closedCount = + grievances.where((g) => g.status == GrievanceStatus.closed).length; + + // Category distribution + final categoryDistribution = {}; + for (final grievance in grievances) { + categoryDistribution[grievance.category] = + (categoryDistribution[grievance.category] ?? 0) + 1; + } + + // Department distribution (using category as department for now) + final departmentDistribution = Map.from(categoryDistribution); + + // Status distribution + final statusDistribution = { + 'pending': pendingCount, + 'underReview': underReviewCount, + 'inProgress': inProgressCount, + 'resolved': resolvedCount, + 'closed': closedCount, + }; + + // Priority distribution + final priorityDistribution = {}; + for (final grievance in grievances) { + final priority = grievance.priority.name; + priorityDistribution[priority] = + (priorityDistribution[priority] ?? 0) + 1; + } + + // Calculate average resolution time + final resolvedGrievances = grievances.where( + (g) => g.status == GrievanceStatus.resolved, + ); + double averageResolutionTime = 0.0; + if (resolvedGrievances.isNotEmpty) { + final totalDays = resolvedGrievances + .map((g) => g.resolvedDate?.difference(g.submittedDate).inDays ?? 0) + .reduce((a, b) => a + b); + averageResolutionTime = totalDays / resolvedGrievances.length; + } + + // Monthly stats (simplified - last 6 months) + final monthlyStats = []; + final now = DateTime.now(); + for (int i = 5; i >= 0; i--) { + final monthDate = DateTime(now.year, now.month - i, 1); + final monthName = _getMonthName(monthDate.month); + + final monthGrievances = grievances.where( + (g) => + g.submittedDate.year == monthDate.year && + g.submittedDate.month == monthDate.month, + ); + + final monthResolved = monthGrievances.where( + (g) => g.status == GrievanceStatus.resolved, + ); + + monthlyStats.add( + MonthlyStats( + month: monthName, + year: monthDate.year, + totalSubmitted: monthGrievances.length, + totalResolved: monthResolved.length, + ), + ); + } + + return Analytics( + totalGrievances: grievances.length, + resolvedGrievances: resolvedCount, + pendingGrievances: pendingCount, + inProgressGrievances: inProgressCount, + averageResolutionTime: averageResolutionTime, + grievancesByCategory: categoryDistribution, + grievancesByDepartment: departmentDistribution, + grievancesByStatus: statusDistribution, + grievancesByPriority: priorityDistribution, + monthlyStats: monthlyStats, + ); + } + + static String _getMonthName(int month) { + const months = [ + '', + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; + return months[month]; + } + + static Analytics generateAnalytics() { + final grievances = GrievanceService.getAllGrievances(); + + // Basic counts + final totalGrievances = grievances.length; + final resolvedGrievances = + grievances.where((g) => g.status == GrievanceStatus.resolved).length; + final pendingGrievances = + grievances.where((g) => g.status == GrievanceStatus.pending).length; + final inProgressGrievances = + grievances.where((g) => g.status == GrievanceStatus.inProgress).length; + + // Average resolution time + final resolvedGrievancesList = + grievances.where((g) => g.resolvedDate != null).toList(); + double averageResolutionTime = 0.0; + if (resolvedGrievancesList.isNotEmpty) { + final totalDays = resolvedGrievancesList.fold(0, (sum, g) { + return sum + g.resolvedDate!.difference(g.submittedDate).inDays; + }); + averageResolutionTime = totalDays / resolvedGrievancesList.length; + } + + // Grievances by category + final grievancesByCategory = {}; + for (final grievance in grievances) { + grievancesByCategory[grievance.category] = + (grievancesByCategory[grievance.category] ?? 0) + 1; + } + + // Grievances by department + final grievancesByDepartment = {}; + for (final grievance in grievances) { + grievancesByDepartment[grievance.department] = + (grievancesByDepartment[grievance.department] ?? 0) + 1; + } + + // Grievances by status + final grievancesByStatus = {}; + for (final status in GrievanceStatus.values) { + final count = grievances.where((g) => g.status == status).length; + grievancesByStatus[status.name] = count; + } + + // Grievances by priority + final grievancesByPriority = {}; + for (final priority in Priority.values) { + final count = grievances.where((g) => g.priority == priority).length; + grievancesByPriority[priority.name] = count; + } + + // Monthly stats for the last 12 months + final monthlyStats = []; + final now = DateTime.now(); + for (int i = 11; i >= 0; i--) { + final month = DateTime(now.year, now.month - i, 1); + final nextMonth = DateTime(month.year, month.month + 1, 1); + + final monthGrievances = + grievances.where((g) { + return g.submittedDate.isAfter(month.subtract(Duration(days: 1))) && + g.submittedDate.isBefore(nextMonth); + }).toList(); + + final resolvedInMonth = + monthGrievances.where((g) { + return g.resolvedDate != null && + g.resolvedDate!.isAfter(month.subtract(Duration(days: 1))) && + g.resolvedDate!.isBefore(nextMonth); + }).length; + + monthlyStats.add( + MonthlyStats( + month: _getMonthName(month.month), + year: month.year, + totalSubmitted: monthGrievances.length, + totalResolved: resolvedInMonth, + ), + ); + } + + return Analytics( + totalGrievances: totalGrievances, + resolvedGrievances: resolvedGrievances, + pendingGrievances: pendingGrievances, + inProgressGrievances: inProgressGrievances, + averageResolutionTime: averageResolutionTime, + grievancesByCategory: grievancesByCategory, + grievancesByDepartment: grievancesByDepartment, + grievancesByStatus: grievancesByStatus, + grievancesByPriority: grievancesByPriority, + monthlyStats: monthlyStats, + ); + } + + static Map getCategoryPercentages() { + final analytics = generateAnalytics(); + final total = analytics.totalGrievances.toDouble(); + + if (total == 0) return {}; + + return analytics.grievancesByCategory.map((category, count) { + return MapEntry(category, (count / total) * 100); + }); + } + + static Map getStatusPercentages() { + final analytics = generateAnalytics(); + final total = analytics.totalGrievances.toDouble(); + + if (total == 0) return {}; + + return analytics.grievancesByStatus.map((status, count) { + return MapEntry(status, (count / total) * 100); + }); + } + + static double getResolutionRate() { + final analytics = generateAnalytics(); + if (analytics.totalGrievances == 0) return 0.0; + return (analytics.resolvedGrievances / analytics.totalGrievances) * 100; + } + + static List getTopPriorityGrievances() { + final grievances = GrievanceService.getAllGrievances(); + final urgentAndHigh = + grievances + .where( + (g) => + (g.priority == Priority.urgent || + g.priority == Priority.high) && + g.status != GrievanceStatus.resolved && + g.status != GrievanceStatus.closed, + ) + .toList(); + + urgentAndHigh.sort((a, b) { + // Sort by priority first (urgent > high), then by date + if (a.priority != b.priority) { + return b.priority.index - a.priority.index; + } + return a.submittedDate.compareTo(b.submittedDate); + }); + + return urgentAndHigh.take(10).toList(); + } + + static List getOldestPendingGrievances() { + final grievances = GrievanceService.getAllGrievances(); + final pending = + grievances + .where( + (g) => + g.status == GrievanceStatus.pending || + g.status == GrievanceStatus.underReview, + ) + .toList(); + + pending.sort((a, b) => a.submittedDate.compareTo(b.submittedDate)); + return pending.take(10).toList(); + } +} diff --git a/lib/services/grievance_service.dart b/lib/services/grievance_service.dart new file mode 100644 index 0000000..1629376 --- /dev/null +++ b/lib/services/grievance_service.dart @@ -0,0 +1,319 @@ +import 'dart:convert'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../models/grievance.dart'; +import '../models/user.dart'; +import '../utils/constants.dart'; + +class GrievanceService { + static const String _grievancesKey = 'grievances'; + static const String _usersKey = 'users'; + + static List _grievances = []; + static List _users = []; + + // Initialize with sample data + static Future initialize() async { + await _loadData(); + if (_grievances.isEmpty) { + await _createSampleData(); + } + } + + static Future _loadData() async { + final prefs = await SharedPreferences.getInstance(); + + // Load grievances + final grievancesJson = prefs.getString(_grievancesKey); + if (grievancesJson != null) { + final List grievancesList = json.decode(grievancesJson); + _grievances = grievancesList.map((g) => Grievance.fromJson(g)).toList(); + } + + // Load users + final usersJson = prefs.getString(_usersKey); + if (usersJson != null) { + final List usersList = json.decode(usersJson); + _users = usersList.map((u) => User.fromJson(u)).toList(); + } + } + + static Future _saveData() async { + final prefs = await SharedPreferences.getInstance(); + + // Save grievances + final grievancesJson = json.encode( + _grievances.map((g) => g.toJson()).toList(), + ); + await prefs.setString(_grievancesKey, grievancesJson); + + // Save users + final usersJson = json.encode(_users.map((u) => u.toJson()).toList()); + await prefs.setString(_usersKey, usersJson); + } + + static Future _createSampleData() async { + // Create sample users + final sampleUsers = [ + User( + id: 'user1', + name: 'John Doe', + email: 'john.doe@college.edu', + phone: '+1 234 567 8900', + role: UserRole.student, + studentId: '2021CS001', + department: 'Computer Science', + year: '3rd Year', + createdAt: DateTime.now().subtract(Duration(days: 100)), + lastLogin: DateTime.now().subtract(Duration(hours: 2)), + ), + User( + id: 'admin1', + name: 'Admin User', + email: 'admin@college.edu', + phone: '+1 234 567 8901', + role: UserRole.admin, + department: 'Administration', + createdAt: DateTime.now().subtract(Duration(days: 200)), + lastLogin: DateTime.now().subtract(Duration(minutes: 30)), + ), + ]; + + _users.addAll(sampleUsers); + + // Create sample grievances + final sampleGrievances = [ + Grievance( + id: Utils.generateGrievanceId(), + title: 'Library WiFi Issues', + description: + 'The WiFi in the library is very slow and frequently disconnects.', + category: 'Library Issues', + priority: Priority.medium, + status: GrievanceStatus.resolved, + studentId: '2021CS001', + studentName: 'John Doe', + studentEmail: 'john.doe@college.edu', + department: 'Computer Science', + year: '3rd Year', + submittedDate: DateTime.now().subtract(Duration(days: 10)), + lastUpdated: DateTime.now().subtract(Duration(days: 2)), + resolvedDate: DateTime.now().subtract(Duration(days: 2)), + statusHistory: [ + StatusUpdate( + id: 'status1', + status: GrievanceStatus.pending, + comment: 'Grievance submitted', + timestamp: DateTime.now().subtract(Duration(days: 10)), + updatedBy: 'System', + ), + StatusUpdate( + id: 'status2', + status: GrievanceStatus.underReview, + comment: 'Under review by IT department', + timestamp: DateTime.now().subtract(Duration(days: 8)), + updatedBy: 'Admin User', + ), + StatusUpdate( + id: 'status3', + status: GrievanceStatus.inProgress, + comment: 'Working on improving network infrastructure', + timestamp: DateTime.now().subtract(Duration(days: 5)), + updatedBy: 'IT Team', + ), + StatusUpdate( + id: 'status4', + status: GrievanceStatus.resolved, + comment: 'New routers installed. WiFi speed improved.', + timestamp: DateTime.now().subtract(Duration(days: 2)), + updatedBy: 'IT Team', + ), + ], + rating: 4, + feedback: 'Thanks for the quick resolution!', + ), + Grievance( + id: Utils.generateGrievanceId(), + title: 'Hostel Hot Water Issue', + description: + 'Hot water is not available in the morning hours in Block A.', + category: 'Hostel Problems', + priority: Priority.high, + status: GrievanceStatus.inProgress, + studentId: '2021CS001', + studentName: 'John Doe', + studentEmail: 'john.doe@college.edu', + department: 'Computer Science', + year: '3rd Year', + submittedDate: DateTime.now().subtract(Duration(days: 5)), + lastUpdated: DateTime.now().subtract(Duration(days: 1)), + statusHistory: [ + StatusUpdate( + id: 'status5', + status: GrievanceStatus.pending, + comment: 'Grievance submitted', + timestamp: DateTime.now().subtract(Duration(days: 5)), + updatedBy: 'System', + ), + StatusUpdate( + id: 'status6', + status: GrievanceStatus.underReview, + comment: 'Forwarded to hostel management', + timestamp: DateTime.now().subtract(Duration(days: 3)), + updatedBy: 'Admin User', + ), + StatusUpdate( + id: 'status7', + status: GrievanceStatus.inProgress, + comment: 'Plumber scheduled for tomorrow', + timestamp: DateTime.now().subtract(Duration(days: 1)), + updatedBy: 'Hostel Warden', + ), + ], + ), + ]; + + _grievances.addAll(sampleGrievances); + await _saveData(); + } + + // CRUD Operations + static Future submitGrievance(Grievance grievance) async { + final newGrievance = grievance.copyWith( + id: Utils.generateGrievanceId(), + submittedDate: DateTime.now(), + lastUpdated: DateTime.now(), + statusHistory: [ + StatusUpdate( + id: DateTime.now().millisecondsSinceEpoch.toString(), + status: GrievanceStatus.pending, + comment: 'Grievance submitted', + timestamp: DateTime.now(), + updatedBy: 'System', + ), + ], + ); + + _grievances.add(newGrievance); + await _saveData(); + return newGrievance.id; + } + + static Future updateGrievanceStatus( + String grievanceId, + GrievanceStatus newStatus, + String comment, + String updatedBy, + ) async { + final index = _grievances.indexWhere((g) => g.id == grievanceId); + if (index != -1) { + final grievance = _grievances[index]; + final statusUpdate = StatusUpdate( + id: DateTime.now().millisecondsSinceEpoch.toString(), + status: newStatus, + comment: comment, + timestamp: DateTime.now(), + updatedBy: updatedBy, + ); + + final updatedHistory = List.from(grievance.statusHistory) + ..add(statusUpdate); + + _grievances[index] = grievance.copyWith( + status: newStatus, + lastUpdated: DateTime.now(), + resolvedDate: + newStatus == GrievanceStatus.resolved ? DateTime.now() : null, + statusHistory: updatedHistory, + ); + + await _saveData(); + } + } + + static Future rateGrievance( + String grievanceId, + int rating, + String? feedback, + ) async { + final index = _grievances.indexWhere((g) => g.id == grievanceId); + if (index != -1) { + _grievances[index] = _grievances[index].copyWith( + rating: rating, + feedback: feedback, + ); + await _saveData(); + } + } + + // Query Operations + static List getAllGrievances() { + return List.from(_grievances); + } + + static List getGrievancesByStudent(String studentId) { + return _grievances.where((g) => g.studentId == studentId).toList(); + } + + static Grievance? getGrievanceById(String id) { + try { + return _grievances.firstWhere((g) => g.id == id); + } catch (e) { + return null; + } + } + + static List filterGrievances({ + String? category, + GrievanceStatus? status, + Priority? priority, + String? department, + DateTime? fromDate, + DateTime? toDate, + }) { + return _grievances.where((grievance) { + if (category != null && grievance.category != category) return false; + if (status != null && grievance.status != status) return false; + if (priority != null && grievance.priority != priority) return false; + if (department != null && grievance.department != department) + return false; + if (fromDate != null && grievance.submittedDate.isBefore(fromDate)) + return false; + if (toDate != null && grievance.submittedDate.isAfter(toDate)) + return false; + return true; + }).toList(); + } + + static List searchGrievances(String query) { + final lowerQuery = query.toLowerCase(); + return _grievances.where((grievance) { + return grievance.title.toLowerCase().contains(lowerQuery) || + grievance.description.toLowerCase().contains(lowerQuery) || + grievance.category.toLowerCase().contains(lowerQuery) || + grievance.studentName.toLowerCase().contains(lowerQuery) || + grievance.id.toLowerCase().contains(lowerQuery); + }).toList(); + } + + // User Operations + static User? getCurrentUser() { + // For demo purposes, return the first student user + try { + return _users.firstWhere((u) => u.role == UserRole.student); + } catch (e) { + return null; + } + } + + static List getAllUsers() { + return List.from(_users); + } + + static Future updateUser(User user) async { + final index = _users.indexWhere((u) => u.id == user.id); + if (index != -1) { + _users[index] = user; + await _saveData(); + } + } +} diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart new file mode 100644 index 0000000..e5bd40c --- /dev/null +++ b/lib/services/notification_service.dart @@ -0,0 +1,188 @@ +import 'dart:convert'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../models/notification.dart'; +import '../models/grievance.dart'; + +class NotificationService { + static const String _notificationsKey = 'app_notifications'; + static List _notifications = []; + + static Future initialize() async { + await _loadNotifications(); + } + + static Future _loadNotifications() async { + final prefs = await SharedPreferences.getInstance(); + final notificationsJson = prefs.getString(_notificationsKey); + + if (notificationsJson != null) { + final List notificationsList = json.decode(notificationsJson); + _notifications = + notificationsList.map((n) => AppNotification.fromJson(n)).toList(); + } + } + + static Future _saveNotifications() async { + final prefs = await SharedPreferences.getInstance(); + final notificationsJson = json.encode( + _notifications.map((n) => n.toJson()).toList(), + ); + await prefs.setString(_notificationsKey, notificationsJson); + } + + static Future addNotification(AppNotification notification) async { + _notifications.insert(0, notification); // Add to beginning for latest first + await _saveNotifications(); + } + + static Future markAsRead(String notificationId) async { + final index = _notifications.indexWhere((n) => n.id == notificationId); + if (index != -1) { + _notifications[index] = _notifications[index].copyWith(isRead: true); + await _saveNotifications(); + } + } + + static Future markAllAsRead(String userId) async { + for (int i = 0; i < _notifications.length; i++) { + if (_notifications[i].userId == userId && !_notifications[i].isRead) { + _notifications[i] = _notifications[i].copyWith(isRead: true); + } + } + await _saveNotifications(); + } + + static Future deleteNotification(String notificationId) async { + _notifications.removeWhere((n) => n.id == notificationId); + await _saveNotifications(); + } + + static Future clearAllNotifications(String userId) async { + _notifications.removeWhere((n) => n.userId == userId); + await _saveNotifications(); + } + + static List getNotificationsForUser(String userId) { + return _notifications.where((n) => n.userId == userId).toList(); + } + + static List getUnreadNotifications(String userId) { + return _notifications + .where((n) => n.userId == userId && !n.isRead) + .toList(); + } + + static int getUnreadCount(String userId) { + return getUnreadNotifications(userId).length; + } + + // Helper methods to create specific notifications + static Future notifyStatusUpdate( + String userId, + String grievanceId, + GrievanceStatus newStatus, + String grievanceTitle, + ) async { + final notification = AppNotification( + id: DateTime.now().millisecondsSinceEpoch.toString(), + title: 'Grievance Status Updated', + message: + 'Your grievance "$grievanceTitle" has been updated to ${_getStatusDisplayName(newStatus)}', + userId: userId, + grievanceId: grievanceId, + timestamp: DateTime.now(), + type: NotificationType.statusUpdate, + ); + + await addNotification(notification); + } + + static Future notifyNewGrievance( + String adminUserId, + String grievanceId, + String grievanceTitle, + String studentName, + ) async { + final notification = AppNotification( + id: DateTime.now().millisecondsSinceEpoch.toString(), + title: 'New Grievance Submitted', + message: 'New grievance "$grievanceTitle" submitted by $studentName', + userId: adminUserId, + grievanceId: grievanceId, + timestamp: DateTime.now(), + type: NotificationType.newGrievance, + ); + + await addNotification(notification); + } + + static Future notifyAssignment( + String staffUserId, + String grievanceId, + String grievanceTitle, + ) async { + final notification = AppNotification( + id: DateTime.now().millisecondsSinceEpoch.toString(), + title: 'Grievance Assigned', + message: 'You have been assigned to handle grievance "$grievanceTitle"', + userId: staffUserId, + grievanceId: grievanceId, + timestamp: DateTime.now(), + type: NotificationType.assignment, + ); + + await addNotification(notification); + } + + static Future notifyReminder( + String userId, + String grievanceId, + String grievanceTitle, + int daysPending, + ) async { + final notification = AppNotification( + id: DateTime.now().millisecondsSinceEpoch.toString(), + title: 'Grievance Reminder', + message: + 'Grievance "$grievanceTitle" has been pending for $daysPending days', + userId: userId, + grievanceId: grievanceId, + timestamp: DateTime.now(), + type: NotificationType.reminder, + ); + + await addNotification(notification); + } + + static Future notifyGeneral( + String userId, + String title, + String message, + ) async { + final notification = AppNotification( + id: DateTime.now().millisecondsSinceEpoch.toString(), + title: title, + message: message, + userId: userId, + timestamp: DateTime.now(), + type: NotificationType.general, + ); + + await addNotification(notification); + } + + static String _getStatusDisplayName(GrievanceStatus status) { + switch (status) { + case GrievanceStatus.pending: + return 'Pending'; + case GrievanceStatus.underReview: + return 'Under Review'; + case GrievanceStatus.inProgress: + return 'In Progress'; + case GrievanceStatus.resolved: + return 'Resolved'; + case GrievanceStatus.closed: + return 'Closed'; + } + } +} diff --git a/lib/services/theme_provider.dart b/lib/services/theme_provider.dart new file mode 100644 index 0000000..3ab189e --- /dev/null +++ b/lib/services/theme_provider.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class ThemeProvider extends ChangeNotifier { + static const String _themeKey = 'theme_mode'; + + ThemeMode _themeMode = ThemeMode.light; + + ThemeMode get themeMode => _themeMode; + + bool get isDarkMode => _themeMode == ThemeMode.dark; + + ThemeProvider() { + _loadTheme(); + } + + Future _loadTheme() async { + final prefs = await SharedPreferences.getInstance(); + final themeModeString = prefs.getString(_themeKey) ?? 'light'; + _themeMode = themeModeString == 'dark' ? ThemeMode.dark : ThemeMode.light; + notifyListeners(); + } + + Future toggleTheme() async { + _themeMode = + _themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light; + final prefs = await SharedPreferences.getInstance(); + await prefs.setString( + _themeKey, + _themeMode == ThemeMode.dark ? 'dark' : 'light', + ); + notifyListeners(); + } + + Future setTheme(ThemeMode themeMode) async { + _themeMode = themeMode; + final prefs = await SharedPreferences.getInstance(); + await prefs.setString( + _themeKey, + themeMode == ThemeMode.dark ? 'dark' : 'light', + ); + notifyListeners(); + } +} diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart new file mode 100644 index 0000000..78c2e31 --- /dev/null +++ b/lib/utils/constants.dart @@ -0,0 +1,209 @@ +import 'package:flutter/material.dart'; +import '../models/grievance.dart'; + +class Constants { + // App Information + static const String appName = 'College Grievance System'; + static const String appVersion = '1.0.0'; + + // Categories + static const List grievanceCategories = [ + 'Academic Issues', + 'Hostel Problems', + 'Library Issues', + 'Canteen Complaints', + 'Transport Issues', + 'Administrative Problems', + 'Technical Issues', + 'Infrastructure Issues', + 'Sports & Recreation', + 'Other', + ]; + + // Departments + static const List departments = [ + 'Computer Science', + 'Mechanical Engineering', + 'Electrical Engineering', + 'Civil Engineering', + 'Electronics & Communication', + 'Information Technology', + 'Business Administration', + 'Commerce', + 'Arts', + 'Science', + 'Mathematics', + 'Physics', + 'Chemistry', + 'Biology', + ]; + + // Years of Study + static const List yearsOfStudy = [ + '1st Year', + '2nd Year', + '3rd Year', + '4th Year', + 'Final Year', + 'Post Graduate', + ]; + + // Priority Levels + static const List priorityLevels = [ + 'Low', + 'Medium', + 'High', + 'Urgent', + ]; + + // File Types + static const List allowedFileTypes = [ + 'jpg', + 'jpeg', + 'png', + 'pdf', + 'doc', + 'docx', + 'txt', + ]; + + // Max file size (in bytes) - 10MB + static const int maxFileSize = 10 * 1024 * 1024; + + // Shared Preferences Keys + static const String themeKey = 'theme_mode'; + static const String languageKey = 'language'; + static const String userDataKey = 'user_data'; + static const String notificationsKey = 'notifications_enabled'; +} + +class Utils { + static String getStatusDisplayName(GrievanceStatus status) { + switch (status) { + case GrievanceStatus.pending: + return 'Pending'; + case GrievanceStatus.underReview: + return 'Under Review'; + case GrievanceStatus.inProgress: + return 'In Progress'; + case GrievanceStatus.resolved: + return 'Resolved'; + case GrievanceStatus.closed: + return 'Closed'; + } + } + + static String getPriorityDisplayName(Priority priority) { + switch (priority) { + case Priority.low: + return 'Low'; + case Priority.medium: + return 'Medium'; + case Priority.high: + return 'High'; + case Priority.urgent: + return 'Urgent'; + } + } + + static Color getStatusColor(GrievanceStatus status) { + switch (status) { + case GrievanceStatus.pending: + return Colors.orange; + case GrievanceStatus.underReview: + return Colors.blue; + case GrievanceStatus.inProgress: + return Colors.amber; + case GrievanceStatus.resolved: + return Colors.green; + case GrievanceStatus.closed: + return Colors.grey; + } + } + + static Color getPriorityColor(Priority priority) { + switch (priority) { + case Priority.low: + return Colors.green; + case Priority.medium: + return Colors.orange; + case Priority.high: + return Colors.red; + case Priority.urgent: + return Colors.red[900]!; + } + } + + static IconData getStatusIcon(GrievanceStatus status) { + switch (status) { + case GrievanceStatus.pending: + return Icons.hourglass_empty; + case GrievanceStatus.underReview: + return Icons.visibility; + case GrievanceStatus.inProgress: + return Icons.work; + case GrievanceStatus.resolved: + return Icons.check_circle; + case GrievanceStatus.closed: + return Icons.close; + } + } + + static IconData getPriorityIcon(Priority priority) { + switch (priority) { + case Priority.low: + return Icons.low_priority; + case Priority.medium: + return Icons.priority_high; + case Priority.high: + return Icons.priority_high; + case Priority.urgent: + return Icons.warning; + } + } + + static String formatDate(DateTime date) { + return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'; + } + + static String formatDateTime(DateTime dateTime) { + return '${formatDate(dateTime)} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}'; + } + + static String getTimeDifference(DateTime dateTime) { + final now = DateTime.now(); + final difference = now.difference(dateTime); + + if (difference.inDays > 0) { + return '${difference.inDays} days ago'; + } else if (difference.inHours > 0) { + return '${difference.inHours} hours ago'; + } else if (difference.inMinutes > 0) { + return '${difference.inMinutes} minutes ago'; + } else { + return 'Just now'; + } + } + + static String generateGrievanceId() { + final now = DateTime.now(); + return 'GRV${now.year}${now.month.toString().padLeft(2, '0')}${now.day.toString().padLeft(2, '0')}${now.millisecondsSinceEpoch.toString().substring(8)}'; + } + + static bool isValidEmail(String email) { + return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email); + } + + static bool isValidPhone(String phone) { + return RegExp(r'^\+?[\d\s\-\(\)]{10,}$').hasMatch(phone); + } + + static String getFileExtension(String fileName) { + return fileName.split('.').last.toLowerCase(); + } + + static bool isValidFileType(String fileName) { + final extension = getFileExtension(fileName); + return Constants.allowedFileTypes.contains(extension); + } +} diff --git a/lib/utils/theme.dart b/lib/utils/theme.dart new file mode 100644 index 0000000..6559e6a --- /dev/null +++ b/lib/utils/theme.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + static ThemeData lightTheme = ThemeData( + brightness: Brightness.light, + primarySwatch: Colors.blue, + primaryColor: Colors.blue[600], + scaffoldBackgroundColor: Colors.grey[50], + appBarTheme: AppBarTheme( + backgroundColor: Colors.blue[600], + foregroundColor: Colors.white, + elevation: 2, + centerTitle: true, + ), + cardTheme: CardThemeData( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue[600], + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), + ), + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + filled: true, + fillColor: Colors.white, + ), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: Colors.white, + selectedItemColor: Colors.blue[600], + unselectedItemColor: Colors.grey[600], + type: BottomNavigationBarType.fixed, + ), + ); + + static ThemeData darkTheme = ThemeData( + brightness: Brightness.dark, + primarySwatch: Colors.blue, + primaryColor: Colors.blue[400], + scaffoldBackgroundColor: Colors.grey[900], + appBarTheme: AppBarTheme( + backgroundColor: Colors.grey[850], + foregroundColor: Colors.white, + elevation: 2, + centerTitle: true, + ), + cardTheme: CardThemeData( + elevation: 4, + color: Colors.grey[800], + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue[400], + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), + ), + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + filled: true, + fillColor: Colors.grey[800], + ), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: Colors.grey[850], + selectedItemColor: Colors.blue[400], + unselectedItemColor: Colors.grey[400], + type: BottomNavigationBarType.fixed, + ), + ); +} diff --git a/lib/widgets/common_widgets.dart b/lib/widgets/common_widgets.dart new file mode 100644 index 0000000..e4e8bb1 --- /dev/null +++ b/lib/widgets/common_widgets.dart @@ -0,0 +1,293 @@ +import 'package:flutter/material.dart'; +import '../models/grievance.dart'; +import '../utils/constants.dart'; + +class StatusTimeline extends StatelessWidget { + final List statusHistory; + + const StatusTimeline({super.key, required this.statusHistory}); + + @override + Widget build(BuildContext context) { + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: statusHistory.length, + itemBuilder: (context, index) { + final status = statusHistory[index]; + final isLast = index == statusHistory.length - 1; + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Utils.getStatusColor(status.status), + ), + child: Icon( + Utils.getStatusIcon(status.status), + color: Colors.white, + size: 12, + ), + ), + if (!isLast) + Container(width: 2, height: 40, color: Colors.grey[300]), + ], + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + Utils.getStatusDisplayName(status.status), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 4), + if (status.comment.isNotEmpty) + Text( + status.comment, + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + const SizedBox(height: 4), + Row( + children: [ + Text( + Utils.formatDateTime(status.timestamp), + style: TextStyle(fontSize: 12, color: Colors.grey[500]), + ), + const Spacer(), + Text( + 'by ${status.updatedBy}', + style: TextStyle( + fontSize: 12, + color: Colors.grey[500], + fontStyle: FontStyle.italic, + ), + ), + ], + ), + if (!isLast) const SizedBox(height: 16), + ], + ), + ), + ], + ); + }, + ); + } +} + +class SearchAndFilterBar extends StatefulWidget { + final String searchQuery; + final String? selectedCategory; + final String? selectedStatus; + final String? selectedPriority; + final Function(String) onSearchChanged; + final Function(String?) onCategoryChanged; + final Function(String?) onStatusChanged; + final Function(String?) onPriorityChanged; + final VoidCallback onClearFilters; + + const SearchAndFilterBar({ + super.key, + required this.searchQuery, + required this.selectedCategory, + required this.selectedStatus, + required this.selectedPriority, + required this.onSearchChanged, + required this.onCategoryChanged, + required this.onStatusChanged, + required this.onPriorityChanged, + required this.onClearFilters, + }); + + @override + State createState() => _SearchAndFilterBarState(); +} + +class _SearchAndFilterBarState extends State { + late TextEditingController _searchController; + bool _isExpanded = false; + + final List _categories = [ + 'Academic', + 'Administrative', + 'Infrastructure', + 'Hostel', + 'Canteen', + 'Library', + 'Sports', + 'Other', + ]; + + @override + void initState() { + super.initState(); + _searchController = TextEditingController(text: widget.searchQuery); + } + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'Search grievances...', + prefixIcon: const Icon(Icons.search), + suffixIcon: + widget.searchQuery.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _searchController.clear(); + widget.onSearchChanged(''); + }, + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + ), + ), + onChanged: widget.onSearchChanged, + ), + ), + const SizedBox(width: 8), + IconButton( + icon: Icon( + _isExpanded ? Icons.filter_list_off : Icons.filter_list, + ), + onPressed: () { + setState(() { + _isExpanded = !_isExpanded; + }); + }, + ), + if (_hasActiveFilters()) + IconButton( + icon: const Icon(Icons.clear_all), + onPressed: () { + _searchController.clear(); + widget.onClearFilters(); + }, + ), + ], + ), + if (_isExpanded) ...[ + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: DropdownButtonFormField( + value: widget.selectedCategory, + decoration: const InputDecoration( + labelText: 'Category', + border: OutlineInputBorder(), + ), + items: [ + const DropdownMenuItem( + value: null, + child: Text('All Categories'), + ), + ..._categories.map( + (category) => DropdownMenuItem( + value: category, + child: Text(category), + ), + ), + ], + onChanged: widget.onCategoryChanged, + ), + ), + const SizedBox(width: 8), + Expanded( + child: DropdownButtonFormField( + value: widget.selectedStatus, + decoration: const InputDecoration( + labelText: 'Status', + border: OutlineInputBorder(), + ), + items: [ + const DropdownMenuItem( + value: null, + child: Text('All Statuses'), + ), + ...GrievanceStatus.values.map( + (status) => DropdownMenuItem( + value: status.name, + child: Text(status.name.toUpperCase()), + ), + ), + ], + onChanged: widget.onStatusChanged, + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: DropdownButtonFormField( + value: widget.selectedPriority, + decoration: const InputDecoration( + labelText: 'Priority', + border: OutlineInputBorder(), + ), + items: [ + const DropdownMenuItem( + value: null, + child: Text('All Priorities'), + ), + ...Priority.values.map( + (priority) => DropdownMenuItem( + value: priority.name, + child: Text(priority.name.toUpperCase()), + ), + ), + ], + onChanged: widget.onPriorityChanged, + ), + ), + const SizedBox(width: 8), + Expanded(child: Container()), // Spacer + ], + ), + ], + ], + ), + ), + ); + } + + bool _hasActiveFilters() { + return widget.selectedCategory != null || + widget.selectedStatus != null || + widget.selectedPriority != null || + widget.searchQuery.isNotEmpty; + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } +} diff --git a/lib/widgets/grievance_card.dart b/lib/widgets/grievance_card.dart new file mode 100644 index 0000000..e3ff4bb --- /dev/null +++ b/lib/widgets/grievance_card.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; +import '../models/grievance.dart'; +import '../utils/constants.dart'; + +class GrievanceCard extends StatelessWidget { + final Grievance grievance; + final VoidCallback? onTap; + final bool showActions; + + const GrievanceCard({ + super.key, + required this.grievance, + this.onTap, + this.showActions = false, + }); + + @override + Widget build(BuildContext context) { + return Card( + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + grievance.title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Utils.getStatusColor(grievance.status), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + Utils.getStatusDisplayName(grievance.status), + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + grievance.description, + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 12), + Row( + children: [ + Icon(Icons.category, size: 16, color: Colors.grey[600]), + const SizedBox(width: 4), + Expanded( + child: Text( + grievance.category, + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Utils.getPriorityColor( + grievance.priority, + ).withOpacity(0.1), + border: Border.all( + color: Utils.getPriorityColor(grievance.priority), + width: 1, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + Utils.getPriorityDisplayName(grievance.priority), + style: TextStyle( + fontSize: 10, + color: Utils.getPriorityColor(grievance.priority), + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Icon(Icons.access_time, size: 16, color: Colors.grey[600]), + const SizedBox(width: 4), + Text( + Utils.getTimeDifference(grievance.submittedDate), + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + const Spacer(), + Text( + 'ID: ${grievance.id.substring(grievance.id.length - 6)}', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + fontWeight: FontWeight.bold, + ), + ), + ], + ), + if (showActions) ...[ + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () { + // TODO: Implement update status + }, + icon: const Icon(Icons.edit, size: 16), + label: const Text('Update'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 8), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton.icon( + onPressed: () { + // TODO: Implement view details + }, + icon: const Icon(Icons.visibility, size: 16), + label: const Text('View'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 8), + ), + ), + ), + ], + ), + ], + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/quick_action_card.dart b/lib/widgets/quick_action_card.dart new file mode 100644 index 0000000..8c6f399 --- /dev/null +++ b/lib/widgets/quick_action_card.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +class QuickActionCard extends StatelessWidget { + final String title; + final String subtitle; + final IconData icon; + final Color color; + final VoidCallback? onTap; + + const QuickActionCard({ + super.key, + required this.title, + required this.subtitle, + required this.icon, + required this.color, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 160, + child: Card( + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon(icon, size: 24, color: color), + ), + const SizedBox(height: 12), + Text( + title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 4), + Text( + subtitle, + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/stat_card.dart b/lib/widgets/stat_card.dart new file mode 100644 index 0000000..e16447f --- /dev/null +++ b/lib/widgets/stat_card.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +class StatCard extends StatelessWidget { + final String title; + final String value; + final IconData icon; + final Color color; + final String? trend; + final bool isUrgent; + + const StatCard({ + super.key, + required this.title, + required this.value, + required this.icon, + required this.color, + this.trend, + this.isUrgent = false, + }); + + @override + Widget build(BuildContext context) { + return Card( + elevation: isUrgent ? 4 : 2, + color: isUrgent ? Colors.red.shade50 : null, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, color: isUrgent ? Colors.red : color, size: 24), + const Spacer(), + if (isUrgent) Icon(Icons.warning, color: Colors.red, size: 16), + ], + ), + const SizedBox(height: 8), + Text( + value, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: isUrgent ? Colors.red : null, + ), + ), + const SizedBox(height: 4), + Text( + title, + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + if (trend != null) ...[ + const SizedBox(height: 4), + Text( + trend!, + style: TextStyle( + fontSize: 12, + color: trend!.startsWith('+') ? Colors.green : Colors.grey, + fontWeight: FontWeight.w500, + ), + ), + ], + ], + ), + ), + ); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..7299b5c 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,14 @@ #include "generated_plugin_registrant.h" +#include +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..786ff5c 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..aa53084 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,16 @@ import FlutterMacOS import Foundation +import file_picker +import file_selector_macos +import path_provider_foundation +import shared_preferences_foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index d993b91..e6e131d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" boolean_selector: dependency: transitive description: @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" cupertino_icons: dependency: "direct main" description: @@ -49,14 +57,86 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810 + url: "https://pub.dev" + source: hosted + version: "8.3.7" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711" + url: "https://pub.dev" + source: hosted + version: "0.9.4+3" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "0.9.3+4" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "00b74ae680df6b1135bdbea00a7d1fc072a9180b7c3f3702e4b19a9943f5ed7d" + url: "https://pub.dev" + source: hosted + version: "0.66.2" flutter: dependency: "direct main" description: flutter @@ -70,19 +150,125 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e + url: "https://pub.dev" + source: hosted + version: "2.0.28" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + http: + dependency: "direct main" + description: + name: http + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "6fae381e6af2bbe0365a5e4ce1db3959462fa0c4d234facf070746024bb80c8d" + url: "https://pub.dev" + source: hosted + version: "0.8.12+24" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" + url: "https://pub.dev" + source: hosted + version: "0.8.12+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" + url: "https://pub.dev" + source: hosted + version: "2.10.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -131,6 +317,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -139,6 +341,134 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -192,6 +522,78 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + url: "https://pub.dev" + source: hosted + version: "6.3.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" vector_math: dependency: transitive description: @@ -204,10 +606,34 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" + url: "https://pub.dev" + source: hosted + version: "5.14.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "1.1.0" sdks: - dart: ">=3.7.2 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index 163790f..e634243 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,33 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + + # Local storage + shared_preferences: ^2.2.2 + + # File picker and image picker + file_picker: ^8.0.0+1 + image_picker: ^1.0.7 + + # Charts for analytics + fl_chart: ^0.66.2 + + # Internationalization + flutter_localizations: + sdk: flutter + intl: ^0.20.2 + + # HTTP requests + http: ^1.2.0 + + # Path provider for file handling + path_provider: ^2.1.2 + + # URL launcher + url_launcher: ^6.2.4 + + # Provider for state management + provider: ^6.1.1 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..043a96f 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,12 @@ #include "generated_plugin_registrant.h" +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..a95e267 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST