Files
mobile-app/lib/features/agents/presentation/screens/agent_network_screen.dart
pradeepkumar 5f146853bd fix
2026-03-31 23:23:08 +05:30

606 lines
21 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:shimmer/shimmer.dart';
import 'package:real_estate_mobile/core/constants/app_colors.dart';
import 'package:real_estate_mobile/core/widgets/s3_image.dart';
import 'package:real_estate_mobile/features/agents/presentation/providers/agent_network_provider.dart';
import 'package:real_estate_mobile/features/messaging/presentation/providers/messaging_provider.dart';
class AgentNetworkScreen extends ConsumerWidget {
const AgentNetworkScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(agentNetworkProvider);
if (state.isLoading) {
return Shimmer.fromColors(
baseColor: const Color(0xFFE8E8E8),
highlightColor: const Color(0xFFF5F5F5),
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
physics: const NeverScrollableScrollPhysics(),
itemCount: 4,
itemBuilder: (_, __) => Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(children: [
Container(width: 56, height: 56, decoration: const BoxDecoration(shape: BoxShape.circle, color: Colors.white)),
const SizedBox(width: 12),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Container(height: 14, width: 130, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))),
const SizedBox(height: 8),
Container(height: 12, width: 180, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))),
])),
]),
),
),
);
}
if (state.error != null) {
return _buildError(context, ref, state.error!);
}
return _buildContent(context, ref, state);
}
Widget _buildError(BuildContext context, WidgetRef ref, String error) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline,
size: 48, color: AppColors.accentOrange),
const SizedBox(height: 16),
Text(error,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.primaryDark,
)),
const SizedBox(height: 12),
TextButton(
onPressed: () =>
ref.read(agentNetworkProvider.notifier).loadData(),
child: const Text('Try Again',
style: TextStyle(color: AppColors.accentOrange)),
),
],
),
);
}
Widget _buildContent(
BuildContext context, WidgetRef ref, AgentNetworkState state) {
return RefreshIndicator(
color: AppColors.accentOrange,
onRefresh: () => ref.read(agentNetworkProvider.notifier).loadData(),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
// ── Pending Requests Section ──
if (state.pendingRequests.isNotEmpty) ...[
_buildSectionHeader('Pending Requests',
count: state.pendingRequests.length),
const SizedBox(height: 12),
...state.pendingRequests.map((request) => _RequestCard(
data: request,
isProcessing:
state.processingIds.contains(request['id']),
)),
const SizedBox(height: 16),
Divider(
color: AppColors.primaryDark.withValues(alpha: 0.1),
height: 1),
const SizedBox(height: 16),
],
// ── Manage My Network Section ──
_buildManageNetworkHeader(context),
const SizedBox(height: 12),
if (state.connections.isEmpty)
_buildEmptyState(
icon: Icons.people_outline,
message: 'No connections yet',
subtitle:
'When you accept connection requests, they will appear here.',
)
else
...state.connections
.map((conn) => _ConnectionCard(data: conn)),
const SizedBox(height: 24),
],
),
),
);
}
Widget _buildSectionHeader(String title, {int? count}) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
Text(
title,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.primaryDark,
),
),
if (count != null && count > 0) ...[
const SizedBox(width: 8),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: AppColors.accentOrange,
borderRadius: BorderRadius.circular(10),
),
child: Text(
'$count',
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
],
],
),
);
}
Widget _buildManageNetworkHeader(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 0),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
decoration: BoxDecoration(
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.1),
width: 0.5),
borderRadius: BorderRadius.circular(7),
),
child: Row(
children: [
GestureDetector(
onTap: () {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
} else {
context.go('/home');
}
},
child: const Icon(
Icons.arrow_back_ios_new_rounded,
size: 18,
color: AppColors.primaryDark,
),
),
const SizedBox(width: 12),
const Text(
'Manage My Network',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.primaryDark,
),
),
],
),
);
}
Widget _buildEmptyState({
required IconData icon,
required String message,
String? subtitle,
}) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 48, color: AppColors.primaryDark.withValues(alpha: 0.3)),
const SizedBox(height: 12),
Text(
message,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.primaryDark,
),
),
if (subtitle != null) ...[
const SizedBox(height: 6),
Text(
subtitle,
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark.withValues(alpha: 0.6),
),
),
],
],
),
),
);
}
}
// ── Relative time formatting (same as web) ──
String _formatRelativeTime(String? dateStr) {
if (dateStr == null || dateStr.isEmpty) return '';
try {
final date = DateTime.parse(dateStr);
final now = DateTime.now();
final diff = now.difference(date);
if (diff.inSeconds < 60) return 'Just now';
if (diff.inMinutes < 60) return '${diff.inMinutes}m ago';
if (diff.inHours < 24) return '${diff.inHours}h ago';
if (diff.inDays < 7) return '${diff.inDays}d ago';
return DateFormat('MMM d').format(date);
} catch (_) {
return '';
}
}
// ── Date formatting for connections ──
String _formatConnectedDate(String? dateStr) {
if (dateStr == null || dateStr.isEmpty) return '';
try {
final date = DateTime.parse(dateStr);
return 'Connected on ${DateFormat('MMM d, y').format(date)}';
} catch (_) {
return '';
}
}
// ── Request Card (pending connection requests with accept/reject) ──
class _RequestCard extends ConsumerWidget {
final Map<String, dynamic> data;
final bool isProcessing;
const _RequestCard({required this.data, required this.isProcessing});
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = data['user'] as Map<String, dynamic>? ?? {};
final userProfile = user['userProfile'] as Map<String, dynamic>? ?? {};
final profileFirstName = userProfile['firstName'] as String? ?? '';
final profileLastName = userProfile['lastName'] as String? ?? '';
final profileName = '$profileFirstName $profileLastName'.trim();
final userName = profileName.isNotEmpty
? profileName
: (user['email'] as String? ?? 'User');
final avatar = (userProfile['avatar'] as String?) ??
(user['avatar'] as String?);
final message = data['message'] as String?;
final requestId = data['id'] as String;
final createdAt = data['createdAt'] as String?;
final relativeTime = _formatRelativeTime(createdAt);
return Opacity(
opacity: isProcessing ? 0.5 : 1.0,
child: AbsorbPointer(
absorbing: isProcessing,
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
// Avatar
ClipOval(
child: SizedBox(
width: 56,
height: 56,
child: avatar != null && avatar.isNotEmpty
? S3Image(
imageUrl: avatar,
width: 56,
height: 56,
fit: BoxFit.cover,
)
: Container(
color: const Color(0xFFE0E0E0),
child: const Icon(Icons.person,
size: 28, color: AppColors.primaryDark),
),
),
),
const SizedBox(width: 12),
// Name + message + time
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
userName,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 4),
Icon(Icons.verified_user_outlined,
size: 16,
color: AppColors.primaryDark
.withValues(alpha: 0.5)),
],
),
if (message != null && message.isNotEmpty)
Text(
message,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Row(
children: [
Icon(Icons.link,
size: 14,
color: AppColors.primaryDark
.withValues(alpha: 0.5)),
const SizedBox(width: 4),
const Text(
'Mutual Connection',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w200,
color: AppColors.primaryDark,
),
),
if (relativeTime.isNotEmpty) ...[
const Spacer(),
Text(
relativeTime,
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 12,
color: AppColors.primaryDark
.withValues(alpha: 0.5),
),
),
],
],
),
],
),
),
const SizedBox(width: 8),
// Reject button (X)
GestureDetector(
onTap: () => ref
.read(agentNetworkProvider.notifier)
.rejectRequest(requestId),
child: Container(
width: 33,
height: 31,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color:
AppColors.primaryDark.withValues(alpha: 0.3),
width: 1),
),
child: Icon(Icons.close,
size: 18,
color:
AppColors.primaryDark.withValues(alpha: 0.6)),
),
),
const SizedBox(width: 10),
// Accept button (checkmark)
GestureDetector(
onTap: () => ref
.read(agentNetworkProvider.notifier)
.acceptRequest(requestId),
child: Container(
width: 33,
height: 31,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppColors.accentOrange,
),
child: const Icon(Icons.check,
size: 20, color: Colors.white),
),
),
],
),
),
),
);
}
}
// ── Connection Card (accepted connections in Manage My Network) ──
class _ConnectionCard extends ConsumerStatefulWidget {
final Map<String, dynamic> data;
const _ConnectionCard({required this.data});
@override
ConsumerState<_ConnectionCard> createState() => _ConnectionCardState();
}
class _ConnectionCardState extends ConsumerState<_ConnectionCard> {
bool _isStartingChat = false;
Future<void> _openChat() async {
if (_isStartingChat) return;
final userId = widget.data['userId'] as String?;
if (userId == null) {
context.push('/messages');
return;
}
setState(() => _isStartingChat = true);
try {
final conversationId = await ref
.read(messagingProvider.notifier)
.startConversationWithUser(userId);
if (!mounted) return;
if (conversationId != null) {
context.push('/messages/chat/$conversationId');
} else {
context.push('/messages');
}
} catch (_) {
if (mounted) context.push('/messages');
} finally {
if (mounted) setState(() => _isStartingChat = false);
}
}
@override
Widget build(BuildContext context) {
final user = widget.data['user'] as Map<String, dynamic>? ?? {};
final userProfile = user['userProfile'] as Map<String, dynamic>? ?? {};
final profileFirstName = userProfile['firstName'] as String? ?? '';
final profileLastName = userProfile['lastName'] as String? ?? '';
final profileName = '$profileFirstName $profileLastName'.trim();
final userName = profileName.isNotEmpty
? profileName
: (user['email'] as String? ?? 'User');
final userEmail = user['email'] as String? ?? '';
final avatar = (userProfile['avatar'] as String?) ??
(user['avatar'] as String?);
final respondedAt = widget.data['respondedAt'] as String?;
final connectedDate = _formatConnectedDate(respondedAt);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
// Avatar
ClipOval(
child: SizedBox(
width: 56,
height: 56,
child: avatar != null && avatar.isNotEmpty
? S3Image(
imageUrl: avatar,
width: 56,
height: 56,
fit: BoxFit.cover,
)
: Container(
color: const Color(0xFFE0E0E0),
child: const Icon(Icons.person,
size: 28, color: AppColors.primaryDark),
),
),
),
const SizedBox(width: 12),
// Name + email + connected date
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
userName,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.primaryDark,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 4),
Icon(Icons.verified_user_outlined,
size: 16,
color: AppColors.primaryDark
.withValues(alpha: 0.5)),
],
),
if (userEmail.isNotEmpty)
Text(
userEmail,
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 13,
color:
AppColors.primaryDark.withValues(alpha: 0.6),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (connectedDate.isNotEmpty)
Text(
connectedDate,
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 12,
color:
AppColors.primaryDark.withValues(alpha: 0.5),
),
),
],
),
),
const SizedBox(width: 8),
// Message button
GestureDetector(
onTap: _isStartingChat ? null : _openChat,
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.accentOrange.withValues(alpha: 0.1),
),
child: _isStartingChat
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.accentOrange,
),
)
: const Icon(Icons.chat_bubble_outline,
size: 18, color: AppColors.accentOrange),
),
),
],
),
);
}
}