diff --git a/frontend/lib/screens/pages/invitation_details_page.dart b/frontend/lib/screens/pages/invitation_details_page.dart index fb6c195..23234d2 100644 --- a/frontend/lib/screens/pages/invitation_details_page.dart +++ b/frontend/lib/screens/pages/invitation_details_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../../models/invitation_models.dart'; import '../../services/invitations_service.dart'; +import '../../services/user_service.dart'; import '../../utils/invitation_utils.dart'; class InvitationDetailsPage extends StatefulWidget { @@ -23,11 +24,14 @@ class _InvitationDetailsPageState extends State { InvitationDetails? _invitationDetails; bool _isLoading = true; bool _isCancelling = false; + bool _isAccepting = false; + bool _isCurrentlyParticipant = false; String? _errorMessage; @override void initState() { super.initState(); + _isCurrentlyParticipant = widget.isParticipant; _loadInvitationDetails(); } @@ -48,6 +52,11 @@ class _InvitationDetailsPageState extends State { _errorMessage = result['message']; } }); + + // Update participation status after loading details + if (result['success']) { + await _updateParticipationStatus(); + } } } @@ -150,6 +159,52 @@ class _InvitationDetailsPageState extends State { } } + Future _updateParticipationStatus() async { + if (_invitationDetails == null) return; + + final userResult = await UserService.getCurrentUser(); + if (userResult['success'] && userResult['data'] != null) { + final currentUserId = userResult['data']['id']; + final isParticipant = _invitationDetails!.attendees.any((attendee) => attendee.id == currentUserId); + setState(() { + _isCurrentlyParticipant = isParticipant; + }); + } + } + + Future _acceptInvitation() async { + setState(() { + _isAccepting = true; + }); + + final result = await InvitationsService.acceptInvitation(widget.invitationId); + + if (mounted) { + setState(() { + _isAccepting = false; + }); + + if (result['success']) { + // Reload invitation details to reflect the new state + await _loadInvitationDetails(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Invitation accepted successfully!'), + backgroundColor: Colors.green, + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(result['message'] ?? 'Failed to accept invitation'), + backgroundColor: Colors.red, + ), + ); + } + } + } + @override Widget build(BuildContext context) { return PopScope( @@ -509,7 +564,45 @@ class _InvitationDetailsPageState extends State { ), ), ], - if (widget.isParticipant) ...[ + + // Accept button for non-participants who are not owners + if (!_isCurrentlyParticipant && !widget.isOwner) ...[ + SizedBox(height: 32), + Container( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: _isAccepting ? null : _acceptInvitation, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF6A4C93), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + ), + child: _isAccepting + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text( + 'Accept Invitation', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + + // Cancel/Leave button for participants + if (_isCurrentlyParticipant) ...[ SizedBox(height: 32), Container( width: double.infinity, diff --git a/frontend/lib/screens/pages/invitations_page.dart b/frontend/lib/screens/pages/invitations_page.dart index 2acb672..8d7f364 100644 --- a/frontend/lib/screens/pages/invitations_page.dart +++ b/frontend/lib/screens/pages/invitations_page.dart @@ -49,7 +49,9 @@ class _InvitationsPageState extends State _isLoading = false; _errorMessage = null; }); - print('Invitations UI updated with ${updatedInvitationsData.created.length + updatedInvitationsData.accepted.length + updatedInvitationsData.available.length} total invitations'); + print( + 'Invitations UI updated with ${updatedInvitationsData.created.length + updatedInvitationsData.accepted.length + updatedInvitationsData.available.length} total invitations', + ); } }, onError: (error) { @@ -69,7 +71,9 @@ class _InvitationsPageState extends State }); } - final result = await InvitationsService.getAllInvitations(forceRefresh: forceRefresh); + final result = await InvitationsService.getAllInvitations( + forceRefresh: forceRefresh, + ); if (mounted) { setState(() { @@ -537,70 +541,73 @@ class _InvitationsPageState extends State } }, child: Scaffold( - appBar: AppBar( - title: Text( - 'Invitations', - style: TextStyle(fontWeight: FontWeight.w600), + appBar: AppBar( + title: Text( + 'Invitations', + style: TextStyle(fontWeight: FontWeight.w600), + ), + backgroundColor: Colors.white, + foregroundColor: Color(0xFF6A4C93), + elevation: 0, + bottom: PreferredSize( + preferredSize: Size.fromHeight(1), + child: Container(height: 1, color: Colors.grey[200]), + ), + automaticallyImplyLeading: false, + actions: [ + IconButton( + icon: Icon(Icons.refresh), + onPressed: () => _loadInvitations(forceRefresh: true), + ), + ], ), - backgroundColor: Colors.white, - foregroundColor: Color(0xFF6A4C93), - elevation: 0, - bottom: PreferredSize( - preferredSize: Size.fromHeight(1), - child: Container(height: 1, color: Colors.grey[200]), - ), - automaticallyImplyLeading: false, - actions: [ - IconButton(icon: Icon(Icons.refresh), onPressed: () => _loadInvitations(forceRefresh: true)), - ], - ), - body: _isLoading - ? Center(child: CircularProgressIndicator(color: Color(0xFF6A4C93))) - : _errorMessage != null - ? _buildErrorState() - : _invitationsData?.isEmpty ?? true - ? _buildEmptyState() - : RefreshIndicator( - onRefresh: () => _loadInvitations(forceRefresh: true), - color: Color(0xFF6A4C93), - child: SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: Column( - children: [ - SizedBox(height: 16), - _buildInvitationSection( - 'My Invitations', - _invitationsData?.created ?? [], - 'created', - ), - _buildInvitationSection( - 'Accepted Invitations', - _invitationsData?.accepted ?? [], - 'accepted', - ), - _buildInvitationSection( - 'Available Invitations', - _invitationsData?.available ?? [], - 'available', - ), - SizedBox(height: 80), - ], + body: _isLoading + ? Center(child: CircularProgressIndicator(color: Color(0xFF6A4C93))) + : _errorMessage != null + ? _buildErrorState() + : _invitationsData?.isEmpty ?? true + ? _buildEmptyState() + : RefreshIndicator( + onRefresh: () => _loadInvitations(forceRefresh: true), + color: Color(0xFF6A4C93), + child: SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + SizedBox(height: 16), + _buildInvitationSection( + 'My Invitations', + _invitationsData?.created ?? [], + 'created', + ), + _buildInvitationSection( + 'Accepted Invitations', + _invitationsData?.accepted ?? [], + 'accepted', + ), + _buildInvitationSection( + 'Available Invitations', + _invitationsData?.available ?? [], + 'available', + ), + SizedBox(height: 80), + ], + ), ), ), - ), - floatingActionButton: FloatingActionButton( - onPressed: () async { - final result = await Navigator.push( - context, - MaterialPageRoute(builder: (context) => CreateInvitationPage()), - ); - if (result == true) { - _loadInvitations(forceRefresh: true); - } - }, - backgroundColor: Color(0xFF6A4C93), - child: Icon(Icons.add, color: Colors.white), - ), + floatingActionButton: FloatingActionButton( + onPressed: () async { + final result = await Navigator.push( + context, + MaterialPageRoute(builder: (context) => CreateInvitationPage()), + ); + if (result == true) { + _loadInvitations(forceRefresh: true); + } + }, + backgroundColor: Color(0xFF6A4C93), + child: Icon(Icons.add, color: Colors.white), + ), ), ); } @@ -747,366 +754,366 @@ class _CreateInvitationPageState extends State { canPop: true, // Allow back navigation to go back to invitations page only child: Scaffold( body: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Color(0xFF32B0A5), Color(0xFF4600B9)], - stops: [0.0, 0.5], + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFF32B0A5), Color(0xFF4600B9)], + stops: [0.0, 0.5], + ), ), - ), - child: SafeArea( - child: Column( - children: [ - Padding( - padding: EdgeInsets.all(16), - child: Row( - children: [ - IconButton( - onPressed: () => Navigator.of(context).pop(), - icon: Icon(Icons.arrow_back, color: Colors.white), - ), - Text( - 'Create Invitation', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - color: Colors.white, + child: SafeArea( + child: Column( + children: [ + Padding( + padding: EdgeInsets.all(16), + child: Row( + children: [ + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: Icon(Icons.arrow_back, color: Colors.white), ), - ), - ], - ), - ), - Expanded( - child: Container( - margin: EdgeInsets.all(16), - padding: EdgeInsets.all(24), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 20, - offset: Offset(0, 5), + Text( + 'Create Invitation', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Colors.white, + ), ), ], ), - child: Form( - key: _formKey, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - 'Create New Invitation', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - textAlign: TextAlign.center, - ), - SizedBox(height: 24), - - TextFormField( - controller: _titleController, - decoration: InputDecoration( - labelText: 'Title *', - prefixIcon: Icon(Icons.title), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), + ), + Expanded( + child: Container( + margin: EdgeInsets.all(16), + padding: EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: Offset(0, 5), + ), + ], + ), + child: Form( + key: _formKey, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Create New Invitation', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: Color(0xFF6A4C93), + textAlign: TextAlign.center, + ), + SizedBox(height: 24), + + TextFormField( + controller: _titleController, + decoration: InputDecoration( + labelText: 'Title *', + prefixIcon: Icon(Icons.title), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Color(0xFF6A4C93), + ), + ), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Please enter a title'; + } + return null; + }, + ), + SizedBox(height: 16), + + TextFormField( + controller: _descriptionController, + maxLines: 3, + decoration: InputDecoration( + labelText: 'Description *', + prefixIcon: Icon(Icons.description), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Color(0xFF6A4C93), + ), + ), + alignLabelWithHint: true, + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Please enter a description'; + } + return null; + }, + ), + SizedBox(height: 16), + + Row( + children: [ + Expanded( + child: InkWell( + onTap: _selectDate, + child: Container( + padding: EdgeInsets.symmetric( + vertical: 16, + horizontal: 12, + ), + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey[400]!, + ), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + 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, + fontSize: 16, + ), + ), + ], + ), + ), + ), + ), + SizedBox(width: 12), + Expanded( + child: InkWell( + onTap: _selectTime, + child: Container( + padding: EdgeInsets.symmetric( + vertical: 16, + horizontal: 12, + ), + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey[400]!, + ), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + 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, + fontSize: 16, + ), + ), + ], + ), + ), + ), + ), + ], + ), + SizedBox(height: 16), + + TextFormField( + controller: _locationController, + decoration: InputDecoration( + labelText: 'Location (Optional)', + prefixIcon: Icon(Icons.location_on), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Color(0xFF6A4C93), + ), ), ), ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Please enter a title'; - } - return null; - }, - ), - SizedBox(height: 16), + SizedBox(height: 16), - TextFormField( - controller: _descriptionController, - maxLines: 3, - decoration: InputDecoration( - labelText: 'Description *', - prefixIcon: Icon(Icons.description), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: Color(0xFF6A4C93), + TextFormField( + controller: _maxParticipantsController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: 'Maximum Participants *', + prefixIcon: Icon(Icons.people), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Color(0xFF6A4C93), + ), ), ), - alignLabelWithHint: true, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Please enter maximum participants'; + } + final intValue = int.tryParse(value); + if (intValue == null || intValue < 1) { + return 'Please enter a valid number greater than 0'; + } + return null; + }, ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Please enter a description'; - } - return null; - }, - ), - SizedBox(height: 16), + SizedBox(height: 20), - Row( - children: [ - Expanded( - child: InkWell( - onTap: _selectDate, + Text( + 'Select Tag *', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + SizedBox(height: 12), + + Wrap( + spacing: 8, + runSpacing: 8, + children: List.generate(_availableTags.length, ( + index, + ) { + final tag = _availableTags[index]; + final isSelected = _selectedTagIndex == index; + return GestureDetector( + onTap: () { + setState(() { + _selectedTagIndex = index; + }); + }, child: Container( padding: EdgeInsets.symmetric( - vertical: 16, horizontal: 12, + vertical: 8, ), decoration: BoxDecoration( - border: Border.all( - color: Colors.grey[400]!, - ), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - children: [ - 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, - fontSize: 16, - ), - ), - ], - ), - ), - ), - ), - SizedBox(width: 12), - Expanded( - child: InkWell( - onTap: _selectTime, - child: Container( - padding: EdgeInsets.symmetric( - vertical: 16, - horizontal: 12, - ), - decoration: BoxDecoration( - border: Border.all( - color: Colors.grey[400]!, - ), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - children: [ - 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, - fontSize: 16, - ), - ), - ], - ), - ), - ), - ), - ], - ), - SizedBox(height: 16), - - TextFormField( - controller: _locationController, - decoration: InputDecoration( - labelText: 'Location (Optional)', - prefixIcon: Icon(Icons.location_on), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: Color(0xFF6A4C93), - ), - ), - ), - ), - SizedBox(height: 16), - - TextFormField( - controller: _maxParticipantsController, - keyboardType: TextInputType.number, - decoration: InputDecoration( - labelText: 'Maximum Participants *', - prefixIcon: Icon(Icons.people), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: Color(0xFF6A4C93), - ), - ), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Please enter maximum participants'; - } - final intValue = int.tryParse(value); - if (intValue == null || intValue < 1) { - return 'Please enter a valid number greater than 0'; - } - return null; - }, - ), - SizedBox(height: 20), - - Text( - 'Select Tag *', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.black87, - ), - ), - SizedBox(height: 12), - - Wrap( - spacing: 8, - runSpacing: 8, - children: List.generate(_availableTags.length, ( - index, - ) { - final tag = _availableTags[index]; - final isSelected = _selectedTagIndex == index; - return GestureDetector( - onTap: () { - setState(() { - _selectedTagIndex = index; - }); - }, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - decoration: BoxDecoration( - color: isSelected - ? InvitationUtils.getColorFromHex( - tag['color_hex'], - ) - : Colors.grey[100], - borderRadius: BorderRadius.circular(20), - border: Border.all( color: isSelected ? InvitationUtils.getColorFromHex( tag['color_hex'], ) - : Colors.grey[300]!, - width: 2, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - InvitationUtils.getIconFromName( - tag['icon_name'], - ), - size: 18, + : Colors.grey[100], + borderRadius: BorderRadius.circular(20), + border: Border.all( color: isSelected - ? Colors.white - : InvitationUtils.getColorFromHex( + ? InvitationUtils.getColorFromHex( tag['color_hex'], - ), + ) + : Colors.grey[300]!, + width: 2, ), - SizedBox(width: 6), - Text( - tag['name'], - style: TextStyle( + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + InvitationUtils.getIconFromName( + tag['icon_name'], + ), + size: 18, color: isSelected ? Colors.white - : Colors.black87, - fontWeight: FontWeight.w500, + : InvitationUtils.getColorFromHex( + tag['color_hex'], + ), + ), + SizedBox(width: 6), + Text( + tag['name'], + style: TextStyle( + color: isSelected + ? Colors.white + : Colors.black87, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + }), + ), + SizedBox(height: 32), + + Container( + height: 56, + child: ElevatedButton( + onPressed: _isSubmitting ? null : _handleSubmit, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF6A4C93), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + ), + child: _isSubmitting + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: + AlwaysStoppedAnimation( + Colors.white, + ), + ), + ) + : Text( + 'Create Invitation', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, ), ), - ], - ), - ), - ); - }), - ), - SizedBox(height: 32), - - Container( - height: 56, - child: ElevatedButton( - onPressed: _isSubmitting ? null : _handleSubmit, - style: ElevatedButton.styleFrom( - backgroundColor: Color(0xFF6A4C93), - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 2, ), - child: _isSubmitting - ? SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: - AlwaysStoppedAnimation( - Colors.white, - ), - ), - ) - : Text( - 'Create Invitation', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), ), - ), - ], + ], + ), ), ), ), ), - ), - ], + ], + ), ), ), - ), ), ); }