feat: add an accept button in view invitations page

This commit is contained in:
sBubshait 2025-07-28 11:24:58 +03:00
parent c9316b5190
commit 80a280a4aa
2 changed files with 486 additions and 386 deletions

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../models/invitation_models.dart'; import '../../models/invitation_models.dart';
import '../../services/invitations_service.dart'; import '../../services/invitations_service.dart';
import '../../services/user_service.dart';
import '../../utils/invitation_utils.dart'; import '../../utils/invitation_utils.dart';
class InvitationDetailsPage extends StatefulWidget { class InvitationDetailsPage extends StatefulWidget {
@ -23,11 +24,14 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
InvitationDetails? _invitationDetails; InvitationDetails? _invitationDetails;
bool _isLoading = true; bool _isLoading = true;
bool _isCancelling = false; bool _isCancelling = false;
bool _isAccepting = false;
bool _isCurrentlyParticipant = false;
String? _errorMessage; String? _errorMessage;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_isCurrentlyParticipant = widget.isParticipant;
_loadInvitationDetails(); _loadInvitationDetails();
} }
@ -48,6 +52,11 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
_errorMessage = result['message']; _errorMessage = result['message'];
} }
}); });
// Update participation status after loading details
if (result['success']) {
await _updateParticipationStatus();
}
} }
} }
@ -150,6 +159,52 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
} }
} }
Future<void> _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<void> _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PopScope( return PopScope(
@ -509,7 +564,45 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
), ),
), ),
], ],
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<Color>(Colors.white),
),
)
: Text(
'Accept Invitation',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
// Cancel/Leave button for participants
if (_isCurrentlyParticipant) ...[
SizedBox(height: 32), SizedBox(height: 32),
Container( Container(
width: double.infinity, width: double.infinity,

View File

@ -49,7 +49,9 @@ class _InvitationsPageState extends State<InvitationsPage>
_isLoading = false; _isLoading = false;
_errorMessage = null; _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) { onError: (error) {
@ -69,7 +71,9 @@ class _InvitationsPageState extends State<InvitationsPage>
}); });
} }
final result = await InvitationsService.getAllInvitations(forceRefresh: forceRefresh); final result = await InvitationsService.getAllInvitations(
forceRefresh: forceRefresh,
);
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -537,70 +541,73 @@ class _InvitationsPageState extends State<InvitationsPage>
} }
}, },
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
'Invitations', 'Invitations',
style: TextStyle(fontWeight: FontWeight.w600), 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, body: _isLoading
foregroundColor: Color(0xFF6A4C93), ? Center(child: CircularProgressIndicator(color: Color(0xFF6A4C93)))
elevation: 0, : _errorMessage != null
bottom: PreferredSize( ? _buildErrorState()
preferredSize: Size.fromHeight(1), : _invitationsData?.isEmpty ?? true
child: Container(height: 1, color: Colors.grey[200]), ? _buildEmptyState()
), : RefreshIndicator(
automaticallyImplyLeading: false, onRefresh: () => _loadInvitations(forceRefresh: true),
actions: [ color: Color(0xFF6A4C93),
IconButton(icon: Icon(Icons.refresh), onPressed: () => _loadInvitations(forceRefresh: true)), child: SingleChildScrollView(
], physics: AlwaysScrollableScrollPhysics(),
), child: Column(
body: _isLoading children: [
? Center(child: CircularProgressIndicator(color: Color(0xFF6A4C93))) SizedBox(height: 16),
: _errorMessage != null _buildInvitationSection(
? _buildErrorState() 'My Invitations',
: _invitationsData?.isEmpty ?? true _invitationsData?.created ?? [],
? _buildEmptyState() 'created',
: RefreshIndicator( ),
onRefresh: () => _loadInvitations(forceRefresh: true), _buildInvitationSection(
color: Color(0xFF6A4C93), 'Accepted Invitations',
child: SingleChildScrollView( _invitationsData?.accepted ?? [],
physics: AlwaysScrollableScrollPhysics(), 'accepted',
child: Column( ),
children: [ _buildInvitationSection(
SizedBox(height: 16), 'Available Invitations',
_buildInvitationSection( _invitationsData?.available ?? [],
'My Invitations', 'available',
_invitationsData?.created ?? [], ),
'created', SizedBox(height: 80),
), ],
_buildInvitationSection( ),
'Accepted Invitations',
_invitationsData?.accepted ?? [],
'accepted',
),
_buildInvitationSection(
'Available Invitations',
_invitationsData?.available ?? [],
'available',
),
SizedBox(height: 80),
],
), ),
), ),
), floatingActionButton: FloatingActionButton(
floatingActionButton: FloatingActionButton( onPressed: () async {
onPressed: () async { final result = await Navigator.push(
final result = await Navigator.push( context,
context, MaterialPageRoute(builder: (context) => CreateInvitationPage()),
MaterialPageRoute(builder: (context) => CreateInvitationPage()), );
); if (result == true) {
if (result == true) { _loadInvitations(forceRefresh: true);
_loadInvitations(forceRefresh: true); }
} },
}, backgroundColor: Color(0xFF6A4C93),
backgroundColor: Color(0xFF6A4C93), child: Icon(Icons.add, color: Colors.white),
child: Icon(Icons.add, color: Colors.white), ),
),
), ),
); );
} }
@ -747,366 +754,366 @@ class _CreateInvitationPageState extends State<CreateInvitationPage> {
canPop: true, // Allow back navigation to go back to invitations page only canPop: true, // Allow back navigation to go back to invitations page only
child: Scaffold( child: Scaffold(
body: Container( body: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [Color(0xFF32B0A5), Color(0xFF4600B9)], colors: [Color(0xFF32B0A5), Color(0xFF4600B9)],
stops: [0.0, 0.5], stops: [0.0, 0.5],
),
), ),
), child: SafeArea(
child: SafeArea( child: Column(
child: Column( children: [
children: [ Padding(
Padding( padding: EdgeInsets.all(16),
padding: EdgeInsets.all(16), child: Row(
child: Row( children: [
children: [ IconButton(
IconButton( onPressed: () => Navigator.of(context).pop(),
onPressed: () => Navigator.of(context).pop(), icon: Icon(Icons.arrow_back, color: Colors.white),
icon: Icon(Icons.arrow_back, color: Colors.white),
),
Text(
'Create Invitation',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.white,
), ),
), Text(
], 'Create Invitation',
), style: TextStyle(
), fontSize: 20,
Expanded( fontWeight: FontWeight.w600,
child: Container( color: Colors.white,
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, Expanded(
child: SingleChildScrollView( child: Container(
child: Column( margin: EdgeInsets.all(16),
crossAxisAlignment: CrossAxisAlignment.stretch, padding: EdgeInsets.all(24),
children: [ decoration: BoxDecoration(
Text( color: Colors.white,
'Create New Invitation', borderRadius: BorderRadius.circular(16),
style: TextStyle( boxShadow: [
fontSize: 24, BoxShadow(
fontWeight: FontWeight.bold, color: Colors.black.withOpacity(0.1),
color: Colors.black87, blurRadius: 20,
), offset: Offset(0, 5),
textAlign: TextAlign.center, ),
), ],
SizedBox(height: 24), ),
child: Form(
TextFormField( key: _formKey,
controller: _titleController, child: SingleChildScrollView(
decoration: InputDecoration( child: Column(
labelText: 'Title *', crossAxisAlignment: CrossAxisAlignment.stretch,
prefixIcon: Icon(Icons.title), children: [
border: OutlineInputBorder( Text(
borderRadius: BorderRadius.circular(12), 'Create New Invitation',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
), ),
focusedBorder: OutlineInputBorder( textAlign: TextAlign.center,
borderRadius: BorderRadius.circular(12), ),
borderSide: BorderSide( SizedBox(height: 24),
color: Color(0xFF6A4C93),
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) { SizedBox(height: 16),
if (value == null || value.trim().isEmpty) {
return 'Please enter a title';
}
return null;
},
),
SizedBox(height: 16),
TextFormField( TextFormField(
controller: _descriptionController, controller: _maxParticipantsController,
maxLines: 3, keyboardType: TextInputType.number,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Description *', labelText: 'Maximum Participants *',
prefixIcon: Icon(Icons.description), prefixIcon: Icon(Icons.people),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide( borderSide: BorderSide(
color: Color(0xFF6A4C93), 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) { SizedBox(height: 20),
if (value == null || value.trim().isEmpty) {
return 'Please enter a description';
}
return null;
},
),
SizedBox(height: 16),
Row( Text(
children: [ 'Select Tag *',
Expanded( style: TextStyle(
child: InkWell( fontSize: 16,
onTap: _selectDate, 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( child: Container(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
vertical: 16,
horizontal: 12, horizontal: 12,
vertical: 8,
), ),
decoration: BoxDecoration( 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 color: isSelected
? InvitationUtils.getColorFromHex( ? InvitationUtils.getColorFromHex(
tag['color_hex'], tag['color_hex'],
) )
: Colors.grey[300]!, : Colors.grey[100],
width: 2, borderRadius: BorderRadius.circular(20),
), border: Border.all(
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
InvitationUtils.getIconFromName(
tag['icon_name'],
),
size: 18,
color: isSelected color: isSelected
? Colors.white ? InvitationUtils.getColorFromHex(
: InvitationUtils.getColorFromHex(
tag['color_hex'], tag['color_hex'],
), )
: Colors.grey[300]!,
width: 2,
), ),
SizedBox(width: 6), ),
Text( child: Row(
tag['name'], mainAxisSize: MainAxisSize.min,
style: TextStyle( children: [
Icon(
InvitationUtils.getIconFromName(
tag['icon_name'],
),
size: 18,
color: isSelected color: isSelected
? Colors.white ? Colors.white
: Colors.black87, : InvitationUtils.getColorFromHex(
fontWeight: FontWeight.w500, 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<Color>(
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<Color>(
Colors.white,
),
),
)
: Text(
'Create Invitation',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
), ),
), ],
], ),
), ),
), ),
), ),
), ),
), ],
], ),
), ),
), ),
),
), ),
); );
} }