Files
mobile-app/lib/features/profile/presentation/widgets/notifications_tab.dart

363 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:real_estate_mobile/core/constants/app_colors.dart';
import 'package:real_estate_mobile/features/profile/presentation/providers/profile_provider.dart';
import 'package:real_estate_mobile/features/profile/presentation/widgets/profile_shared_widgets.dart';
class NotificationsTab extends ConsumerStatefulWidget {
const NotificationsTab({super.key});
@override
ConsumerState<NotificationsTab> createState() => _NotificationsTabState();
}
class _NotificationsTabState extends ConsumerState<NotificationsTab> {
final Map<String, Map<String, bool>> _notificationPrefs = {
'newLeadAlerts': {'email': true, 'inApp': true},
'messageUpdates': {'email': false, 'inApp': true},
'urgentRequests': {'email': true, 'inApp': true},
};
String _digestFrequency = 'instant';
bool _notificationsLoaded = false;
bool _isLoadingNotifications = false;
bool _isSavingNotifications = false;
@override
void initState() {
super.initState();
_loadNotificationPreferences();
}
Future<void> _loadNotificationPreferences() async {
if (_notificationsLoaded) return;
setState(() => _isLoadingNotifications = true);
try {
final role = ref.read(profileProvider).role;
final repo = ref.read(profileRepositoryProvider);
final data = await repo.getNotificationPreferences(role);
final notifications =
data['notifications'] as Map<String, dynamic>? ?? {};
setState(() {
for (final key in ['new_lead_alerts', 'message_updates', 'urgent_requests_tours']) {
final mapped = key == 'new_lead_alerts'
? 'newLeadAlerts'
: key == 'message_updates'
? 'messageUpdates'
: 'urgentRequests';
final vals = notifications[key] as Map<String, dynamic>?;
if (vals != null) {
_notificationPrefs[mapped] = {
'email': vals['email'] as bool? ?? false,
'inApp': vals['inApp'] as bool? ?? true,
};
}
}
_digestFrequency = data['digestFrequency'] as String? ?? 'instant';
_isLoadingNotifications = false;
_notificationsLoaded = true;
});
} catch (_) {
setState(() {
_isLoadingNotifications = false;
_notificationsLoaded = true;
});
}
}
@override
Widget build(BuildContext context) {
if (_isLoadingNotifications) {
return const Center(
child: CircularProgressIndicator(color: AppColors.accentOrange));
}
return ListView(
padding: const EdgeInsets.fromLTRB(27, 10, 27, 30),
children: [
const Text(
'Notification Preferences',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 15,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 8),
const Text(
'Choose what you want to be notified about and how.',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 20),
_buildSectionDivider(),
const SizedBox(height: 19),
_buildNotificationCategory(
title: 'New Lead Alerts',
description: 'Notifications about new leads and inquiries.',
prefKey: 'newLeadAlerts',
),
_buildSectionDivider(),
const SizedBox(height: 13),
_buildNotificationCategory(
title: 'Message Updates',
description:
'Notices about new messages from clients or other agents.',
prefKey: 'messageUpdates',
),
_buildSectionDivider(),
const SizedBox(height: 16),
_buildNotificationCategory(
title: 'Urgent Requests & Tours',
description:
'Immediate notifications for tour requests and time-sensitive items.',
prefKey: 'urgentRequests',
),
_buildSectionDivider(),
const SizedBox(height: 20),
const Text(
'Email Digest Frequency',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 8),
const Text(
'Choose how often you receive non-urgent email summaries',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 16),
_buildFrequencySelector(),
const SizedBox(height: 40),
ProfileActionButtons(
onSave: _saveNotificationSettings,
onCancel: () => context.go('/home'),
isSaving: _isSavingNotifications,
),
],
);
}
Future<void> _saveNotificationSettings() async {
setState(() => _isSavingNotifications = true);
try {
final role = ref.read(profileProvider).role;
final repo = ref.read(profileRepositoryProvider);
await repo.updateNotificationPreferences(role, {
'notifications': {
'new_lead_alerts': _notificationPrefs['newLeadAlerts'],
'message_updates': _notificationPrefs['messageUpdates'],
'urgent_requests_tours': _notificationPrefs['urgentRequests'],
},
'digestFrequency': _digestFrequency,
});
_showSnackBar('Notification settings saved');
} catch (e) {
_showSnackBar('Failed to save: $e', isError: true);
} finally {
if (mounted) setState(() => _isSavingNotifications = false);
}
}
Widget _buildSectionDivider() {
return Divider(
color: AppColors.primaryDark.withValues(alpha: 0.15),
height: 1,
thickness: 1,
);
}
Widget _buildNotificationCategory({
required String title,
required String description,
required String prefKey,
}) {
final prefs = _notificationPrefs[prefKey]!;
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
)),
const SizedBox(height: 4),
Text(description,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
)),
],
),
),
const SizedBox(width: 20),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildCheckboxRow(
label: 'Email',
value: prefs['email']!,
onChanged: (val) {
setState(() {
_notificationPrefs[prefKey]!['email'] = val ?? false;
});
},
),
const SizedBox(height: 6),
_buildCheckboxRow(
label: 'In-App',
value: prefs['inApp']!,
onChanged: (val) {
setState(() {
_notificationPrefs[prefKey]!['inApp'] = val ?? false;
});
},
),
],
),
],
),
);
}
Widget _buildCheckboxRow({
required String label,
required bool value,
required ValueChanged<bool?> onChanged,
}) {
return GestureDetector(
onTap: () => onChanged(!value),
behavior: HitTestBehavior.opaque,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 18,
height: 18,
child: Checkbox(
value: value,
onChanged: onChanged,
activeColor: AppColors.primaryDark,
checkColor: Colors.white,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(3)),
side: BorderSide(
color: AppColors.primaryDark.withValues(alpha: 0.4),
width: 1,
),
),
),
const SizedBox(width: 4),
Text(label,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
)),
],
),
);
}
Widget _buildFrequencySelector() {
const options = ['instant', 'daily', 'weekly', 'off'];
const labels = ['Instant', 'Daily', 'Weekly', 'Off'];
return Container(
height: 32,
width: 278,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(7),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.15),
width: 1,
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Row(
children: List.generate(options.length * 2 - 1, (index) {
if (index.isOdd) {
return Container(
width: 1,
height: 32,
color: AppColors.primaryDark.withValues(alpha: 0.15),
);
}
final i = index ~/ 2;
final isSelected = _digestFrequency == options[i];
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _digestFrequency = options[i]),
child: Center(
child: isSelected
? Container(
height: 24,
margin: const EdgeInsets.symmetric(horizontal: 3),
decoration: BoxDecoration(
color: AppColors.accentOrange,
borderRadius: BorderRadius.circular(15),
),
alignment: Alignment.center,
child: Text(labels[i],
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 12,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
height: 1.0,
)),
)
: Text(labels[i],
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 12,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
height: 1.0,
)),
),
),
);
}),
),
),
);
}
void _showSnackBar(String message, {bool isError = false}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError ? Colors.red : const Color(0xFF638559),
),
);
}
}