From c060e26b5a8de4d0509d660edc6cc8e7d0519c4a Mon Sep 17 00:00:00 2001 From: sBubshait Date: Tue, 22 Jul 2025 15:46:13 +0300 Subject: [PATCH] feat: add a details page for invitation to see attendees --- .../pages/invitation_details_page.dart | 546 ++++++++++++++++++ 1 file changed, 546 insertions(+) create mode 100644 frontend/lib/screens/pages/invitation_details_page.dart diff --git a/frontend/lib/screens/pages/invitation_details_page.dart b/frontend/lib/screens/pages/invitation_details_page.dart new file mode 100644 index 0000000..3158b75 --- /dev/null +++ b/frontend/lib/screens/pages/invitation_details_page.dart @@ -0,0 +1,546 @@ +import 'package:flutter/material.dart'; +import '../../models/invitation_models.dart'; +import '../../services/invitations_service.dart'; +import '../../utils/invitation_utils.dart'; + +class InvitationDetailsPage extends StatefulWidget { + final int invitationId; + final bool isOwner; + + const InvitationDetailsPage({ + Key? key, + required this.invitationId, + required this.isOwner, + }) : super(key: key); + + @override + _InvitationDetailsPageState createState() => _InvitationDetailsPageState(); +} + +class _InvitationDetailsPageState extends State { + InvitationDetails? _invitationDetails; + bool _isLoading = true; + bool _isCancelling = false; + String? _errorMessage; + + @override + void initState() { + super.initState(); + _loadInvitationDetails(); + } + + Future _loadInvitationDetails() async { + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + final result = await InvitationsService.getInvitationDetails(widget.invitationId); + + if (mounted) { + setState(() { + _isLoading = false; + if (result['success']) { + _invitationDetails = result['data']; + } else { + _errorMessage = result['message']; + } + }); + } + } + + Widget _buildAvatarOrInitial(String displayName, String? avatar) { + return Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Color(0xFF6A4C93).withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: avatar != null + ? ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Image.network( + avatar, + width: 40, + height: 40, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Center( + child: Text( + displayName.isNotEmpty ? displayName[0].toUpperCase() : '?', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFF6A4C93), + ), + ), + ); + }, + ), + ) + : Center( + child: Text( + displayName.isNotEmpty ? displayName[0].toUpperCase() : '?', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFF6A4C93), + ), + ), + ), + ); + } + + Future _cancelInvitation() async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(widget.isOwner ? 'Cancel Invitation' : 'Leave Invitation'), + content: Text( + widget.isOwner + ? 'Are you sure you want to cancel this invitation? This action cannot be undone.' + : 'Are you sure you want to leave this invitation?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text('No'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: Text('Yes'), + ), + ], + ), + ); + + if (confirmed == true) { + setState(() { + _isCancelling = true; + }); + + final result = await InvitationsService.cancelInvitation(widget.invitationId); + + if (mounted) { + setState(() { + _isCancelling = false; + }); + + if (result['success']) { + Navigator.of(context).pop(true); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(result['message'] ?? 'Action completed successfully'), + backgroundColor: Colors.green, + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(result['message'] ?? 'Failed to complete action'), + backgroundColor: Colors.red, + ), + ); + } + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + title: Text( + 'Invitation Details', + 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]), + ), + ), + body: _isLoading + ? Center( + child: CircularProgressIndicator(color: Color(0xFF6A4C93)), + ) + : _errorMessage != null + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 80, color: Colors.red[400]), + SizedBox(height: 16), + Text( + _errorMessage!, + style: TextStyle(fontSize: 16, color: Colors.red[600]), + textAlign: TextAlign.center, + ), + SizedBox(height: 16), + ElevatedButton( + onPressed: _loadInvitationDetails, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF6A4C93), + foregroundColor: Colors.white, + ), + child: Text('Retry'), + ), + ], + ), + ) + : SingleChildScrollView( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + padding: EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 10, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: InvitationUtils.getColorFromHex( + _invitationDetails!.tag.colorHex, + ).withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + ), + child: Icon( + InvitationUtils.getIconFromName( + _invitationDetails!.tag.iconName, + ), + color: InvitationUtils.getColorFromHex( + _invitationDetails!.tag.colorHex, + ), + size: 32, + ), + ), + SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _invitationDetails!.title, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + Container( + margin: EdgeInsets.only(top: 8), + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: InvitationUtils.getColorFromHex( + _invitationDetails!.tag.colorHex, + ).withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + _invitationDetails!.tag.name, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: InvitationUtils.getColorFromHex( + _invitationDetails!.tag.colorHex, + ), + ), + ), + ), + ], + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: _invitationDetails!.status == 'Available' + ? Colors.green.withOpacity(0.1) + : Colors.orange.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + _invitationDetails!.status, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: _invitationDetails!.status == 'Available' + ? Colors.green[700] + : Colors.orange[700], + ), + ), + ), + ], + ), + if (_invitationDetails!.description != null && + _invitationDetails!.description!.isNotEmpty) ...[ + SizedBox(height: 20), + Text( + 'Description', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + SizedBox(height: 8), + Text( + _invitationDetails!.description!, + style: TextStyle( + fontSize: 14, + color: Colors.grey[700], + height: 1.5, + ), + ), + ], + SizedBox(height: 20), + Row( + children: [ + if (_invitationDetails!.location != null) ...[ + Expanded( + child: Row( + children: [ + Icon(Icons.location_on, size: 20, color: Colors.grey[600]), + SizedBox(width: 8), + Expanded( + child: Text( + _invitationDetails!.location!, + style: TextStyle(fontSize: 14, color: Colors.grey[700]), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ], + if (_invitationDetails!.dateTime != null) ...[ + if (_invitationDetails!.location != null) SizedBox(width: 16), + Expanded( + child: Row( + children: [ + Icon(Icons.schedule, size: 20, color: Colors.grey[600]), + SizedBox(width: 8), + Expanded( + child: Text( + InvitationUtils.getRelativeDateTime( + _invitationDetails!.dateTime!, + ), + style: TextStyle(fontSize: 14, color: Colors.grey[700]), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ], + ], + ), + SizedBox(height: 16), + Row( + children: [ + Icon(Icons.people, size: 20, color: Colors.grey[600]), + SizedBox(width: 8), + Text( + '${_invitationDetails!.currentAttendees} / ${_invitationDetails!.maxParticipants} participants', + style: TextStyle(fontSize: 14, color: Colors.grey[700]), + ), + ], + ), + ], + ), + ), + SizedBox(height: 24), + Container( + width: double.infinity, + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 10, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Organizer', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(width: 8), + Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Color(0xFF6A4C93).withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + 'ORGANIZER', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Color(0xFF6A4C93), + ), + ), + ), + ], + ), + SizedBox(height: 12), + Row( + children: [ + _buildAvatarOrInitial( + _invitationDetails!.creator.displayName, + _invitationDetails!.creator.avatar, + ), + SizedBox(width: 12), + Text( + _invitationDetails!.creator.displayName, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + ], + ), + ], + ), + ), + if (_invitationDetails!.attendees.isNotEmpty) ...[ + SizedBox(height: 24), + Container( + width: double.infinity, + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 10, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Attendees (${_invitationDetails!.attendees.length})', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 16), + ...List.generate(_invitationDetails!.attendees.length, (index) { + final attendee = _invitationDetails!.attendees[index]; + return Container( + margin: EdgeInsets.only(bottom: index < _invitationDetails!.attendees.length - 1 ? 12 : 0), + child: Row( + children: [ + _buildAvatarOrInitial( + attendee.displayName, + attendee.avatar, + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + attendee.displayName, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + Text( + 'Joined ${InvitationUtils.getRelativeTime(attendee.joinedAt)}', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ], + ), + ), + ], + ), + ); + }), + ], + ), + ), + ], + SizedBox(height: 32), + Container( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: _isCancelling ? null : _cancelInvitation, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + ), + child: _isCancelling + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text( + widget.isOwner ? 'Cancel Invitation' : 'Leave Invitation', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + SizedBox(height: 32), + ], + ), + ), + ); + } +} \ No newline at end of file