import 'package:flutter/material.dart'; import 'dart:async'; import '../../services/invitations_service.dart'; import '../../models/invitation_models.dart'; import '../../utils/invitation_utils.dart'; import 'invitation_details_page.dart'; class InvitationsPage extends StatefulWidget { @override _InvitationsPageState createState() => _InvitationsPageState(); } class _InvitationsPageState extends State with TickerProviderStateMixin { InvitationsData? _invitationsData; bool _isLoading = true; String? _errorMessage; Map _acceptingInvitations = {}; Map _acceptedInvitations = {}; Map _animationControllers = {}; StreamSubscription? _invitationsStreamSubscription; @override void initState() { super.initState(); _loadInvitations(); _setupInvitationsStream(); } @override void dispose() { for (final controller in _animationControllers.values) { controller.dispose(); } _invitationsStreamSubscription?.cancel(); super.dispose(); } void _setupInvitationsStream() { print('Setting up invitations stream'); _invitationsStreamSubscription = InvitationsService.getInvitationsStream() .listen( (updatedInvitationsData) { print('Invitations stream received updated data'); if (mounted) { // Update the invitations directly instead of fetching again setState(() { _invitationsData = updatedInvitationsData; _isLoading = false; _errorMessage = null; }); print( 'Invitations UI updated with ${updatedInvitationsData.created.length + updatedInvitationsData.accepted.length + updatedInvitationsData.available.length} total invitations', ); } }, onError: (error) { print('Invitations stream error: $error'); }, ); } Future _loadInvitations({bool forceRefresh = false}) async { // Don't show loading for cached data unless forcing refresh final isInitialLoad = _invitationsData == null; if (isInitialLoad || forceRefresh) { setState(() { _isLoading = true; _errorMessage = null; }); } final result = await InvitationsService.getAllInvitations( forceRefresh: forceRefresh, ); if (mounted) { setState(() { if (result['success']) { final invitationsData = result['invitations'] as InvitationsData; _invitationsData = invitationsData; _errorMessage = null; } else { _errorMessage = result['message']; } _isLoading = false; }); } } 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'), ), ], ), ); } Future _acceptInvitation(Invitation invitation) async { final invitationKey = invitation.id.toString(); setState(() { _acceptingInvitations[invitationKey] = true; }); final result = await InvitationsService.acceptInvitation(invitation.id); if (mounted) { setState(() { _acceptingInvitations[invitationKey] = false; }); if (result['success']) { setState(() { _acceptedInvitations[invitationKey] = true; }); final controller = AnimationController( duration: Duration(milliseconds: 1500), vsync: this, ); _animationControllers[invitationKey] = controller; controller.forward(); Future.delayed(Duration(milliseconds: 2000), () { if (mounted) { _loadInvitations(forceRefresh: true); } }); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(result['message'] ?? 'Failed to accept invitation'), backgroundColor: Colors.red, ), ); } } } Future _navigateToInvitationDetails( Invitation invitation, bool isOwner, { bool isParticipant = true, }) async { final result = await Navigator.push( context, MaterialPageRoute( builder: (context) => InvitationDetailsPage( invitationId: invitation.id, isOwner: isOwner, isParticipant: isParticipant, ), ), ); if (result == true) { _loadInvitations(forceRefresh: true); } } Widget _buildInvitationCard(Invitation invitation, String section) { final invitationKey = invitation.id.toString(); bool isOwned = section == 'created'; bool isAccepted = section == 'accepted'; bool isAccepting = _acceptingInvitations[invitationKey] ?? false; bool hasBeenAccepted = _acceptedInvitations[invitationKey] ?? false; AnimationController? animController = _animationControllers[invitationKey]; 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), if (!isOwned && !isAccepted && !hasBeenAccepted) Row( children: [ Expanded( child: SizedBox( height: 44, child: ElevatedButton( onPressed: () { _navigateToInvitationDetails( invitation, false, isParticipant: false, ); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.grey[600], foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), elevation: 2, ), child: Text( 'View', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), ), ), ), SizedBox(width: 12), Expanded( child: SizedBox( height: 44, child: ElevatedButton( onPressed: isAccepting ? null : () { _acceptInvitation(invitation); }, style: ElevatedButton.styleFrom( backgroundColor: Color(0xFF6A4C93), foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), elevation: 2, ), child: isAccepting ? SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( Colors.white, ), ), ) : Text( 'Accept Invite', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), ), ), ), ], ) else SizedBox( width: double.infinity, height: 44, child: ElevatedButton( onPressed: isAccepting ? null : (isOwned || isAccepted || hasBeenAccepted) ? () { _navigateToInvitationDetails(invitation, isOwned); } : () { _acceptInvitation(invitation); }, style: ElevatedButton.styleFrom( backgroundColor: (isAccepted || hasBeenAccepted) ? Colors.green : Color(0xFF6A4C93), foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), elevation: 2, ), child: isAccepting ? SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( Colors.white, ), ), ) : hasBeenAccepted && animController != null ? AnimatedBuilder( animation: animController, builder: (context, child) { return Text( 'View', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ); }, ) : Text( (isAccepted || hasBeenAccepted) ? 'View' : (isOwned ? 'View' : '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 PopScope( canPop: false, // Prevent back navigation from invitations page onPopInvoked: (bool didPop) { // Prevent any pop behavior, including iOS back gesture if (!didPop) { // Do nothing - stay on invitations page } }, child: Scaffold( 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), ), ], ), 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), ), ), ); } } class CreateInvitationPage extends StatefulWidget { @override _CreateInvitationPageState createState() => _CreateInvitationPageState(); } class _CreateInvitationPageState extends State { final _formKey = GlobalKey(); final _titleController = TextEditingController(); final _descriptionController = TextEditingController(); final _locationController = TextEditingController(); final _maxParticipantsController = TextEditingController(); DateTime? _selectedDate; TimeOfDay? _selectedTime; int? _selectedTagIndex; bool _isSubmitting = false; // Use centralized tag configuration from InvitationUtils List> get _availableTags => InvitationUtils.availableTags; @override void dispose() { _titleController.dispose(); _descriptionController.dispose(); _locationController.dispose(); _maxParticipantsController.dispose(); super.dispose(); } Future _selectDate() async { final DateTime? picked = await showDatePicker( context: context, initialDate: DateTime.now(), firstDate: DateTime.now(), lastDate: DateTime.now().add(Duration(days: 365)), builder: (context, child) { return Theme( data: Theme.of(context).copyWith( colorScheme: ColorScheme.light(primary: Color(0xFF6A4C93)), ), child: child!, ); }, ); if (picked != null && picked != _selectedDate) { setState(() { _selectedDate = picked; }); } } Future _selectTime() async { final TimeOfDay? picked = await showTimePicker( context: context, initialTime: TimeOfDay.now(), builder: (context, child) { return Theme( data: Theme.of(context).copyWith( colorScheme: ColorScheme.light(primary: Color(0xFF6A4C93)), ), child: child!, ); }, ); if (picked != null && picked != _selectedTime) { setState(() { _selectedTime = picked; }); } } Future _handleSubmit() async { if (_formKey.currentState!.validate()) { if (_selectedTagIndex == null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Please select a tag for your invitation'), backgroundColor: Colors.red, ), ); return; } setState(() { _isSubmitting = true; }); DateTime? combinedDateTime; if (_selectedDate != null) { if (_selectedTime != null) { combinedDateTime = DateTime( _selectedDate!.year, _selectedDate!.month, _selectedDate!.day, _selectedTime!.hour, _selectedTime!.minute, ); } else { combinedDateTime = _selectedDate; } } final invitationData = { "title": _titleController.text.trim(), "description": _descriptionController.text.trim(), "dateTime": combinedDateTime?.toIso8601String(), "location": _locationController.text.trim().isEmpty ? null : _locationController.text.trim(), "maxParticipants": int.parse(_maxParticipantsController.text), "tagId": _availableTags[_selectedTagIndex!]["id"], }; final result = await InvitationsService.createInvitation(invitationData); if (mounted) { setState(() { _isSubmitting = false; }); if (result['success']) { Navigator.of(context).pop(true); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(result['message'] ?? 'Failed to create invitation'), backgroundColor: Colors.red, ), ); } } } } @override Widget build(BuildContext context) { return PopScope( 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], ), ), 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, ), ), ], ), ), 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, ), 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), ), ), ), ), 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, color: isSelected ? Colors.white : 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, ), ), ), ), ], ), ), ), ), ), ], ), ), ), ), ); } }