diff --git a/frontend/lib/screens/pages/invitations_page.dart b/frontend/lib/screens/pages/invitations_page.dart index b1133ac..84ddc2b 100644 --- a/frontend/lib/screens/pages/invitations_page.dart +++ b/frontend/lib/screens/pages/invitations_page.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; -import '../../services/notification_service.dart'; +import '../../services/invitations_service.dart'; +import '../../models/invitation_models.dart'; +import '../../utils/invitation_utils.dart'; class InvitationsPage extends StatefulWidget { @override @@ -7,63 +9,316 @@ class InvitationsPage extends StatefulWidget { } class _InvitationsPageState extends State { - bool _isAccepted = false; - bool _isLoading = false; + InvitationsData? _invitationsData; + bool _isLoading = true; + String? _errorMessage; - Future _acceptCoffeeInvite() async { + @override + void initState() { + super.initState(); + _loadInvitations(); + } + + Future _loadInvitations() async { setState(() { _isLoading = true; + _errorMessage = null; }); - try { - final success = await NotificationService() - .sendCoffeeInviteAcceptedNotification(); + final result = await InvitationsService.getAllInvitations(); - if (success) { - setState(() { - _isAccepted = true; - _isLoading = false; - }); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Coffee invite accepted! Notification sent to everyone.', - ), - backgroundColor: Colors.green, - duration: Duration(seconds: 3), - ), - ); - } else { - setState(() { - _isLoading = false; - }); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Failed to send notification. Check Firebase service account configuration.', - ), - backgroundColor: Colors.orange, - duration: Duration(seconds: 3), - ), - ); - } - } catch (e) { + if (mounted) { setState(() { _isLoading = false; + if (result['success']) { + _invitationsData = result['data']; + } else { + _errorMessage = result['message']; + } }); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error: $e'), - backgroundColor: Colors.red, - duration: Duration(seconds: 3), - ), - ); } } + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.event_busy, + size: 80, + color: Colors.grey[400], + ), + SizedBox(height: 16), + Text( + 'Nothing here!', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w600, + color: Colors.grey[600], + ), + ), + SizedBox(height: 8), + Text( + 'Create the first invitation now!', + style: TextStyle( + fontSize: 16, + color: Colors.grey[500], + ), + ), + ], + ), + ); + } + + Widget _buildErrorState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 80, + color: Colors.red[400], + ), + SizedBox(height: 16), + Text( + _errorMessage ?? 'Something went wrong', + style: TextStyle( + fontSize: 16, + color: Colors.red[600], + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 16), + ElevatedButton( + onPressed: _loadInvitations, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF6A4C93), + foregroundColor: Colors.white, + ), + child: Text('Retry'), + ), + ], + ), + ); + } + + Widget _buildInvitationCard(Invitation invitation, String section) { + bool isOwned = section == 'created'; + bool isAccepted = section == 'accepted'; + + return Container( + margin: EdgeInsets.only(bottom: 16), + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 10, + offset: Offset(0, 2), + ), + ], + border: Border.all( + color: Colors.grey.withOpacity(0.2), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: InvitationUtils.getColorFromHex(invitation.tag.colorHex).withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + InvitationUtils.getIconFromName(invitation.tag.iconName), + color: InvitationUtils.getColorFromHex(invitation.tag.colorHex), + size: 24, + ), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + invitation.title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + Text( + 'by ${invitation.creator.displayName}', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ], + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: InvitationUtils.getColorFromHex(invitation.tag.colorHex).withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + invitation.tag.name, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: InvitationUtils.getColorFromHex(invitation.tag.colorHex), + ), + ), + ), + ], + ), + if (invitation.description != null && invitation.description!.isNotEmpty) ...[ + SizedBox(height: 12), + Text( + InvitationUtils.truncateDescription(invitation.description), + style: TextStyle( + fontSize: 14, + color: Colors.grey[700], + height: 1.4, + ), + ), + ], + if (invitation.location != null) ...[ + SizedBox(height: 8), + Row( + children: [ + Icon(Icons.location_on, size: 16, color: Colors.grey[600]), + SizedBox(width: 4), + Text( + invitation.location!, + style: TextStyle( + fontSize: 13, + color: Colors.grey[600], + ), + ), + ], + ), + ], + if (invitation.dateTime != null) ...[ + SizedBox(height: 8), + Row( + children: [ + Icon(Icons.schedule, size: 16, color: Colors.grey[600]), + SizedBox(width: 4), + Text( + InvitationUtils.getRelativeDateTime(invitation.dateTime!), + style: TextStyle( + fontSize: 13, + color: Colors.grey[600], + ), + ), + ], + ), + ], + SizedBox(height: 12), + Row( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: InvitationUtils.getParticipantsStatusColor( + invitation.currentAttendees, + invitation.maxParticipants + ).withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + InvitationUtils.getParticipantsStatus( + invitation.currentAttendees, + invitation.maxParticipants + ), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: InvitationUtils.getParticipantsStatusColor( + invitation.currentAttendees, + invitation.maxParticipants + ), + ), + ), + ), + Spacer(), + Text( + InvitationUtils.getRelativeTime(invitation.createdAt), + style: TextStyle(fontSize: 12, color: Colors.grey[500]), + ), + ], + ), + SizedBox(height: 16), + SizedBox( + width: double.infinity, + height: 44, + child: ElevatedButton( + onPressed: isOwned || isAccepted ? null : () { + // TODO: Implement accept invitation + }, + style: ElevatedButton.styleFrom( + backgroundColor: isAccepted + ? Colors.green + : (isOwned ? Colors.grey[400] : Color(0xFF6A4C93)), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: isOwned || isAccepted ? 0 : 2, + ), + child: Text( + isAccepted + ? 'Accepted ✓' + : (isOwned ? 'View/Edit' : 'Accept Invite'), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildInvitationSection(String title, List invitations, String section) { + if (invitations.isEmpty) return SizedBox.shrink(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + title, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Color(0xFF6A4C93), + ), + ), + ), + ...invitations.map((invitation) => Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: _buildInvitationCard(invitation, section), + )).toList(), + ], + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -80,180 +335,47 @@ class _InvitationsPageState extends State { child: Container(height: 1, color: Colors.grey[200]), ), automaticallyImplyLeading: false, + actions: [ + IconButton( + icon: Icon(Icons.refresh), + onPressed: _loadInvitations, + ), + ], ), - body: Padding( - padding: EdgeInsets.all(16), - child: Column( - children: [ - // Coffee Invitation Card - Container( - margin: EdgeInsets.only(bottom: 16), - padding: EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.08), - blurRadius: 10, - offset: Offset(0, 2), - ), - ], - border: Border.all( - color: Colors.grey.withOpacity(0.2), - width: 1, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header with icon and title - Row( - children: [ - Container( - padding: EdgeInsets.all(8), - decoration: BoxDecoration( - color: Color(0xFF6A4C93).withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.coffee, - color: Color(0xFF6A4C93), - size: 24, - ), - ), - SizedBox(width: 12), - Expanded( - child: Text( - 'Who\'s down for coffee?', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.black87, - ), - ), - ), - ], - ), - - SizedBox(height: 16), - - // Description - Text( - 'Quick coffee break at the campus café. Let\'s catch up!', - style: TextStyle( - fontSize: 14, - color: Colors.grey[700], - height: 1.4, - ), - ), - - SizedBox(height: 16), - - // Status indicator - Row( - children: [ - Container( - padding: EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: _isAccepted - ? Colors.green.withOpacity(0.1) - : Colors.orange.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - _isAccepted ? 'Accepted ✓' : '1 more person needed', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: _isAccepted - ? Colors.green[700] - : Colors.orange[700], - ), - ), - ), - Spacer(), - Text( - '10 min ago', - style: TextStyle(fontSize: 12, color: Colors.grey[500]), - ), - ], - ), - - SizedBox(height: 16), - - // Accept button - SizedBox( - width: double.infinity, - height: 44, - child: ElevatedButton( - onPressed: _isAccepted - ? null - : (_isLoading ? null : _acceptCoffeeInvite), - style: ElevatedButton.styleFrom( - backgroundColor: _isAccepted - ? Colors.grey[300] - : Color(0xFF6A4C93), - foregroundColor: _isAccepted - ? Colors.grey[600] - : Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - elevation: _isAccepted ? 0 : 2, - ), - child: _isLoading - ? SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - Colors.white, - ), - strokeWidth: 2, - ), - ) - : Text( - _isAccepted ? 'Accepted' : 'Accept Invite', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), + body: _isLoading + ? Center(child: CircularProgressIndicator(color: Color(0xFF6A4C93))) + : _errorMessage != null + ? _buildErrorState() + : _invitationsData?.isEmpty ?? true + ? _buildEmptyState() + : RefreshIndicator( + onRefresh: _loadInvitations, + color: Color(0xFF6A4C93), + child: SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + SizedBox(height: 16), + _buildInvitationSection( + 'My Invitations', + _invitationsData?.created ?? [], + 'created', ), - ), - ), - ], - ), - ), - - // Info text - if (!_isAccepted) - Container( - padding: EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.05), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.blue.withOpacity(0.2)), - ), - child: Row( - children: [ - Icon(Icons.info_outline, color: Colors.blue[600], size: 20), - SizedBox(width: 8), - Expanded( - child: Text( - 'This is a test invitation :)', - style: TextStyle(fontSize: 13, color: Colors.blue[700]), + _buildInvitationSection( + 'Accepted Invitations', + _invitationsData?.accepted ?? [], + 'accepted', + ), + _buildInvitationSection( + 'Available Invitations', + _invitationsData?.available ?? [], + 'available', + ), + SizedBox(height: 80), + ], + ), ), ), - ], - ), - ), - ], - ), - ), floatingActionButton: FloatingActionButton( onPressed: () { Navigator.push( @@ -279,52 +401,20 @@ class _CreateInvitationPageState extends State { final _descriptionController = TextEditingController(); final _locationController = TextEditingController(); final _maxParticipantsController = TextEditingController(); - + DateTime? _selectedDate; TimeOfDay? _selectedTime; int? _selectedTagIndex; final List> _availableTags = [ - { - "name": "Sports", - "color_hex": "#FF6B35", - "icon_name": "sports_soccer" - }, - { - "name": "Food", - "color_hex": "#F7931E", - "icon_name": "restaurant" - }, - { - "name": "Gaming", - "color_hex": "#FFD23F", - "icon_name": "games" - }, - { - "name": "Study", - "color_hex": "#06FFA5", - "icon_name": "menu_book" - }, - { - "name": "Social", - "color_hex": "#118AB2", - "icon_name": "group" - }, - { - "name": "Travel", - "color_hex": "#06D6A0", - "icon_name": "flight" - }, - { - "name": "Music", - "color_hex": "#8E44AD", - "icon_name": "music_note" - }, - { - "name": "Movies", - "color_hex": "#E74C3C", - "icon_name": "movie" - } + {"name": "Sports", "color_hex": "#FF6B35", "icon_name": "sports_soccer"}, + {"name": "Food", "color_hex": "#F7931E", "icon_name": "restaurant"}, + {"name": "Gaming", "color_hex": "#FFD23F", "icon_name": "games"}, + {"name": "Study", "color_hex": "#06FFA5", "icon_name": "menu_book"}, + {"name": "Social", "color_hex": "#118AB2", "icon_name": "group"}, + {"name": "Travel", "color_hex": "#06D6A0", "icon_name": "flight"}, + {"name": "Music", "color_hex": "#8E44AD", "icon_name": "music_note"}, + {"name": "Movies", "color_hex": "#E74C3C", "icon_name": "movie"}, ]; @override @@ -372,9 +462,7 @@ class _CreateInvitationPageState extends State { builder: (context, child) { return Theme( data: Theme.of(context).copyWith( - colorScheme: ColorScheme.light( - primary: Color(0xFF6A4C93), - ), + colorScheme: ColorScheme.light(primary: Color(0xFF6A4C93)), ), child: child!, ); @@ -394,9 +482,7 @@ class _CreateInvitationPageState extends State { builder: (context, child) { return Theme( data: Theme.of(context).copyWith( - colorScheme: ColorScheme.light( - primary: Color(0xFF6A4C93), - ), + colorScheme: ColorScheme.light(primary: Color(0xFF6A4C93)), ), child: child!, ); @@ -425,8 +511,12 @@ class _CreateInvitationPageState extends State { "title": _titleController.text.trim(), "description": _descriptionController.text.trim(), "date": _selectedDate?.toIso8601String(), - "time": _selectedTime != null ? "${_selectedTime!.hour}:${_selectedTime!.minute.toString().padLeft(2, '0')}" : null, - "location": _locationController.text.trim().isEmpty ? null : _locationController.text.trim(), + "time": _selectedTime != null + ? "${_selectedTime!.hour}:${_selectedTime!.minute.toString().padLeft(2, '0')}" + : null, + "location": _locationController.text.trim().isEmpty + ? null + : _locationController.text.trim(), "max_participants": int.parse(_maxParticipantsController.text), "tag": _availableTags[_selectedTagIndex!], }; @@ -518,7 +608,9 @@ class _CreateInvitationPageState extends State { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Color(0xFF6A4C93)), + borderSide: BorderSide( + color: Color(0xFF6A4C93), + ), ), ), validator: (value) { @@ -541,7 +633,9 @@ class _CreateInvitationPageState extends State { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Color(0xFF6A4C93)), + borderSide: BorderSide( + color: Color(0xFF6A4C93), + ), ), alignLabelWithHint: true, ), @@ -560,21 +654,31 @@ class _CreateInvitationPageState extends State { child: InkWell( onTap: _selectDate, child: Container( - padding: EdgeInsets.symmetric(vertical: 16, horizontal: 12), + padding: EdgeInsets.symmetric( + vertical: 16, + horizontal: 12, + ), decoration: BoxDecoration( - border: Border.all(color: Colors.grey[400]!), + border: Border.all( + color: Colors.grey[400]!, + ), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ - Icon(Icons.calendar_today, color: Colors.grey[600]), + Icon( + Icons.calendar_today, + color: Colors.grey[600], + ), SizedBox(width: 12), Text( _selectedDate == null ? 'Select Date (Optional)' : '${_selectedDate!.day}/${_selectedDate!.month}/${_selectedDate!.year}', style: TextStyle( - color: _selectedDate == null ? Colors.grey[600] : Colors.black87, + color: _selectedDate == null + ? Colors.grey[600] + : Colors.black87, fontSize: 16, ), ), @@ -588,21 +692,31 @@ class _CreateInvitationPageState extends State { child: InkWell( onTap: _selectTime, child: Container( - padding: EdgeInsets.symmetric(vertical: 16, horizontal: 12), + padding: EdgeInsets.symmetric( + vertical: 16, + horizontal: 12, + ), decoration: BoxDecoration( - border: Border.all(color: Colors.grey[400]!), + border: Border.all( + color: Colors.grey[400]!, + ), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ - Icon(Icons.access_time, color: Colors.grey[600]), + Icon( + Icons.access_time, + color: Colors.grey[600], + ), SizedBox(width: 12), Text( _selectedTime == null ? 'Select Time (Optional)' : '${_selectedTime!.hour}:${_selectedTime!.minute.toString().padLeft(2, '0')}', style: TextStyle( - color: _selectedTime == null ? Colors.grey[600] : Colors.black87, + color: _selectedTime == null + ? Colors.grey[600] + : Colors.black87, fontSize: 16, ), ), @@ -625,7 +739,9 @@ class _CreateInvitationPageState extends State { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Color(0xFF6A4C93)), + borderSide: BorderSide( + color: Color(0xFF6A4C93), + ), ), ), ), @@ -642,7 +758,9 @@ class _CreateInvitationPageState extends State { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: Color(0xFF6A4C93)), + borderSide: BorderSide( + color: Color(0xFF6A4C93), + ), ), ), validator: (value) { @@ -671,7 +789,9 @@ class _CreateInvitationPageState extends State { Wrap( spacing: 8, runSpacing: 8, - children: List.generate(_availableTags.length, (index) { + children: List.generate(_availableTags.length, ( + index, + ) { final tag = _availableTags[index]; final isSelected = _selectedTagIndex == index; return GestureDetector( @@ -681,12 +801,19 @@ class _CreateInvitationPageState extends State { }); }, child: Container( - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), decoration: BoxDecoration( - color: isSelected ? _getColorFromHex(tag['color_hex']) : Colors.grey[100], + color: isSelected + ? _getColorFromHex(tag['color_hex']) + : Colors.grey[100], borderRadius: BorderRadius.circular(20), border: Border.all( - color: isSelected ? _getColorFromHex(tag['color_hex']) : Colors.grey[300]!, + color: isSelected + ? _getColorFromHex(tag['color_hex']) + : Colors.grey[300]!, width: 2, ), ), @@ -696,13 +823,19 @@ class _CreateInvitationPageState extends State { Icon( _getIconFromName(tag['icon_name']), size: 18, - color: isSelected ? Colors.white : _getColorFromHex(tag['color_hex']), + color: isSelected + ? Colors.white + : _getColorFromHex( + tag['color_hex'], + ), ), SizedBox(width: 6), Text( tag['name'], style: TextStyle( - color: isSelected ? Colors.white : Colors.black87, + color: isSelected + ? Colors.white + : Colors.black87, fontWeight: FontWeight.w500, ), ), diff --git a/frontend/lib/services/invitations_service.dart b/frontend/lib/services/invitations_service.dart new file mode 100644 index 0000000..49c79ea --- /dev/null +++ b/frontend/lib/services/invitations_service.dart @@ -0,0 +1,50 @@ +import 'dart:convert'; +import '../constants/api_constants.dart'; +import '../models/invitation_models.dart'; +import 'http_service.dart'; + +class InvitationsService { + static Future> getAllInvitations() async { + try { + final response = await HttpService.get(ApiConstants.getAllInvitationsEndpoint); + + if (response.statusCode == 200) { + final responseData = jsonDecode(response.body); + final invitationsResponse = InvitationsResponse.fromJson(responseData); + + if (invitationsResponse.status) { + return { + 'success': true, + 'data': invitationsResponse.data, + }; + } else { + return { + 'success': false, + 'message': invitationsResponse.message ?? 'Failed to fetch invitations', + }; + } + } else if (response.statusCode == 401) { + return { + 'success': false, + 'message': 'Session expired. Please login again.', + }; + } else if (response.statusCode == 403) { + return { + 'success': false, + 'message': 'Access denied. Invalid credentials.', + }; + } else { + return { + 'success': false, + 'message': 'Server error (${response.statusCode})', + }; + } + } catch (e) { + print('Error fetching invitations: $e'); + return { + 'success': false, + 'message': 'Network error. Please check your connection.', + }; + } + } +} \ No newline at end of file