amoriai/lib/screens/profile/profile_screen.dart

762 lines
22 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:firebase_core/firebase_core.dart';
import '../../core/theme/app_theme.dart';
import '../../core/config/app_config.dart';
import '../../core/constants/app_constants.dart';
import '../../models/user_model.dart';
import '../../services/firebase_service.dart';
import '../../services/dialog_service.dart';
import '../auth/login_screen.dart';
import 'preferences_screen.dart';
import 'notification_settings_screen.dart';
import 'privacy_security_screen.dart';
import '../onboarding/daily_checkin_screen.dart';
class ProfileScreen extends ConsumerStatefulWidget {
const ProfileScreen({super.key});
@override
ConsumerState<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends ConsumerState<ProfileScreen> {
UserModel? _userData;
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_loadUserData();
}
Future<void> _loadUserData() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final firebaseService = FirebaseService();
final currentUser = firebaseService.currentUser;
if (currentUser == null) {
throw Exception('Pengguna tidak terautentikasi');
}
final userData = await firebaseService.getUserData(currentUser.uid);
// 🔍 DEBUG — hapus setelah debugging selesai
debugPrint('=== PROFILE DEBUG ===');
debugPrint('userData: ${userData != null ? "OK" : "NULL"}');
debugPrint(
'favoriteCategories: ${userData?.preferences.favoriteCategories}',
);
debugPrint('allergies: ${userData?.preferences.allergies}');
debugPrint('spiceLevel: ${userData?.preferences.spiceLevel}');
debugPrint('budgetRange: ${userData?.preferences.budgetRange}');
debugPrint('dailyPreferences: ${userData?.dailyPreferences}');
debugPrint('====================');
setState(() {
_userData = userData;
_isLoading = false;
});
} catch (e) {
debugPrint('Profile load error: $e');
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.warmWhite,
appBar: AppBar(
title: const Text(
'Profil',
style: TextStyle(
color: AppTheme.primaryBrown,
fontWeight: FontWeight.bold,
),
),
backgroundColor: AppTheme.warmWhite,
elevation: 0,
iconTheme: const IconThemeData(color: AppTheme.primaryBrown),
),
body: _isLoading
? _buildLoadingState()
: _error != null
? _buildErrorState()
: _buildProfileContent(),
);
}
Widget _buildLoadingState() {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Memuat profil...'),
],
),
);
}
Widget _buildErrorState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: AppTheme.error),
const SizedBox(height: 16),
Text(
'Gagal memuat profil',
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
_error ?? 'Terjadi kesalahan',
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: AppTheme.grey600),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _loadUserData,
child: const Text('Coba Lagi'),
),
],
),
);
}
Widget _buildProfileContent() {
final user = FirebaseService().currentUser;
final displayName =
_userData?.displayName ?? user?.displayName ?? 'Pengguna';
final email = _userData?.email ?? user?.email ?? '';
// Extra bottom padding agar konten tidak tertutup navbar (extendBody: true)
final bottomPadding =
MediaQuery.of(context).padding.bottom +
kBottomNavigationBarHeight +
16.0;
return SingleChildScrollView(
padding: EdgeInsets.fromLTRB(
AppConstants.defaultPadding,
AppConstants.defaultPadding,
AppConstants.defaultPadding,
bottomPadding,
),
child: Column(
children: [
// Profile Header
_buildProfileHeader(displayName, email, user?.photoURL),
const SizedBox(height: 32),
// Preferences Summary
_buildPreferencesCard(),
const SizedBox(height: 16),
// Menu Options
_buildMenuOptions(),
const SizedBox(height: 32),
// Sign Out Button
_buildSignOutButton(),
],
),
);
}
Widget _buildProfileHeader(
String displayName,
String email,
String? photoUrl,
) {
final firstName = displayName.split(' ').first;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppTheme.primaryBrown, AppTheme.coffeeBrown],
),
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
boxShadow: [
BoxShadow(
color: AppTheme.primaryBrown.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 8),
),
],
),
child: Column(
children: [
// Profile Picture
CircleAvatar(
radius: 50,
backgroundColor: Colors.white.withOpacity(0.2),
backgroundImage: photoUrl != null
? NetworkImage(photoUrl)
: null,
child: photoUrl == null
? Text(
firstName.isNotEmpty
? firstName[0].toUpperCase()
: 'U',
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
),
)
: null,
),
const SizedBox(height: 16),
// Name
Text(
displayName,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
// Email
Text(
email,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.white.withOpacity(0.9),
),
),
const SizedBox(height: 16),
// Member Since
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Member sejak ${_userData?.createdAt.year ?? DateTime.now().year}',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
Widget _buildPreferencesCard() {
final preferences = _userData?.preferences ?? UserPreferences();
final daily = _userData?.dailyPreferences;
final lastCheckin = _userData?.lastCheckinDate;
// Kategori Favorit
final kategori = preferences.favoriteCategories.isNotEmpty
? preferences.favoriteCategories.join(', ')
: 'Belum diatur';
// Alergi
final alergi = preferences.allergies.isNotEmpty
? preferences.allergies.join(', ')
: 'Tidak ada';
// Mood: dari daily check-in, fallback preferences
String moodText = 'Belum check-in hari ini';
if (daily != null && daily['mood'] != null) {
moodText = daily['mood'].toString();
} else if (preferences.mood.isNotEmpty && preferences.mood != 'neutral') {
moodText = preferences.mood;
}
// Level Pedas
final levelPedas = _formatSpiceLevel(preferences.spiceLevel);
// Budget: dari daily check-in lalu fallback ke preferences
String budgetText = 'Belum diatur';
if (daily != null && daily['budget'] != null) {
final b = daily['budget'];
budgetText = 'Rp ${_formatBudget(b is int ? b : (b as num).toInt())}';
} else if (preferences.budgetRange > 0) {
budgetText = 'Rp ${_formatBudget(preferences.budgetRange)}';
}
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header row
Row(
children: [
Icon(Icons.tune, color: AppTheme.primaryBrown, size: 20),
const SizedBox(width: 8),
Text(
'Preferensi Saya',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.primaryBrown,
),
),
const Spacer(),
TextButton(
onPressed: () => _navigateToPreferences(),
child: const Text('Edit'),
),
],
),
// Sync label
if (lastCheckin != null)
Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(
children: [
Icon(Icons.sync, size: 12, color: AppTheme.grey500),
const SizedBox(width: 4),
Text(
_formatCheckinLabel(lastCheckin),
style: TextStyle(
fontSize: 11,
color: AppTheme.grey500,
fontStyle: FontStyle.italic,
),
),
],
),
),
const SizedBox(height: 8),
_buildPreferenceItem(
'Kategori Favorit',
kategori,
Icons.favorite_outline,
),
_buildPreferenceItem('Alergi', alergi, Icons.warning_outlined),
_buildPreferenceItem(
'Mood Saat Ini',
moodText,
Icons.mood_outlined,
valueColor: AppTheme.primaryBrown,
),
_buildPreferenceItem(
'Level Pedas',
levelPedas,
Icons.local_fire_department_outlined,
),
_buildPreferenceItem(
'Budget',
budgetText,
Icons.account_balance_wallet_outlined,
),
],
),
);
}
/// Label tanggal check-in yang mudah dibaca
String _formatCheckinLabel(DateTime dt) {
final now = DateTime.now();
final isToday =
dt.year == now.year && dt.month == now.month && dt.day == now.day;
if (isToday) return 'Mood diperbarui hari ini';
const months = [
'',
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Agu',
'Sep',
'Okt',
'Nov',
'Des',
];
return 'Terakhir diperbarui ${dt.day} ${months[dt.month]}';
}
/// Format angka ke format Rp (misal 30000 → "30.000")
String _formatBudget(int amount) {
final s = amount.toString();
final buffer = StringBuffer();
int count = 0;
for (int i = s.length - 1; i >= 0; i--) {
if (count > 0 && count % 3 == 0) buffer.write('.');
buffer.write(s[i]);
count++;
}
return buffer.toString().split('').reversed.join();
}
Widget _buildPreferenceItem(
String title,
String value,
IconData icon, {
Color? valueColor,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
Icon(icon, size: 16, color: AppTheme.grey600),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF757575), // grey600
),
),
const SizedBox(height: 2),
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: valueColor ?? const Color(0xFF424242), // grey800
),
),
],
),
),
],
),
);
}
String _formatSpiceLevel(String level) {
switch (level.toLowerCase()) {
case 'mild':
return '🌱 Tidak Pedas';
case 'medium':
return '🌶️ Sedang';
case 'hot':
return '🔥 Pedas';
case 'very hot':
return '🌋 Sangat Pedas';
default:
return 'Belum diatur';
}
}
Widget _buildMenuOptions() {
return Column(
children: [
_buildMenuOption(
icon: Icons.edit_calendar_outlined,
title: 'Isi Ulang Check-in Harian',
subtitle: 'Perbarui mood, keinginan, & kondisi hari ini',
onTap: _navigateToCheckin,
highlight: true,
),
_buildMenuOption(
icon: Icons.notifications_outlined,
title: 'Notifikasi',
subtitle: 'Pengaturan notifikasi aplikasi',
onTap: _showNotificationSettings,
),
_buildMenuOption(
icon: Icons.privacy_tip_outlined,
title: 'Privasi & Keamanan',
subtitle: 'Pengaturan privasi dan keamanan data',
onTap: _showPrivacySettings,
),
_buildMenuOption(
icon: Icons.help_outline,
title: 'Bantuan & FAQ',
subtitle: 'Pertanyaan yang sering diajukan',
onTap: _showHelp,
),
_buildMenuOption(
icon: Icons.info_outline,
title: 'Tentang Aplikasi',
subtitle: 'Informasi aplikasi dan versi',
onTap: _showAbout,
),
],
);
}
Widget _buildMenuOption({
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
bool highlight = false,
}) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: highlight
? AppTheme.primaryBrown.withOpacity(0.06)
: Colors.white,
border: highlight
? Border.all(color: AppTheme.primaryBrown.withOpacity(0.3))
: null,
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: ListTile(
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.primaryBrown.withOpacity(highlight ? 0.2 : 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: AppTheme.primaryBrown, size: 20),
),
title: Text(
title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
color: highlight ? AppTheme.primaryBrown : null,
),
),
subtitle: Text(
subtitle,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: AppTheme.grey600),
),
trailing: const Icon(Icons.chevron_right, color: AppTheme.grey400),
onTap: onTap,
),
);
}
Future<void> _navigateToCheckin() async {
final result = await Navigator.of(context).push<bool>(
MaterialPageRoute(
builder: (_) => const DailyCheckinScreen(isMandatory: false),
),
);
// Refresh halaman profil setelah check-in selesai agar data terbaru tampil
if (result == true && mounted) {
_loadUserData();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text(
'✅ Check-in berhasil! Preferensi profil diperbarui.',
style: TextStyle(fontFamily: 'Poppins'),
),
backgroundColor: AppTheme.primaryBrown,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}
}
}
Widget _buildSignOutButton() {
return SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _signOut,
icon: const Icon(Icons.logout, color: AppTheme.error),
label: const Text('Keluar', style: TextStyle(color: AppTheme.error)),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: AppTheme.error),
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
);
}
void _navigateToPreferences() async {
// Sinkronkan data preferensi dengan daily check-in (jika ada override)
final currentPrefs = _userData?.preferences ?? UserPreferences();
if (_userData?.dailyPreferences?['budget'] != null) {
final overrideBudget = (_userData!.dailyPreferences!['budget'] as num).toInt();
// Hanya ubah untuk UI di PreferencesScreen agar sama persis
currentPrefs.budgetRange = overrideBudget;
}
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => PreferencesScreen(
userPreferences: currentPrefs,
onPreferencesUpdated: (preferences) {
// Update state langsung dari callback (optimistic update)
setState(() {
if (_userData != null) {
_userData = _userData!.copyWith(preferences: preferences);
// Juga sinkronkan state daily override di UI profil
if (_userData!.dailyPreferences != null) {
_userData!.dailyPreferences!['budget'] = preferences.budgetRange;
}
}
});
},
),
),
);
// Setelah kembali dari halaman preferensi, selalu reload dari Firebase
// untuk memastikan data terbaru tersinkron
_loadUserData();
}
void _showNotificationSettings() {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const NotificationSettingsScreen()),
);
}
void _showPrivacySettings() {
Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => const PrivacySecurityScreen()));
}
void _showHelp() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Bantuan & FAQ'),
content: const SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Cara menggunakan aplikasi:',
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('1. Ambil foto menu kafe'),
Text('2. Tunggu AI menganalisis menu'),
Text('3. Dapatkan rekomendasi personal'),
Text('4. Chat dengan AI untuk pertanyaan lanjutan'),
SizedBox(height: 16),
Text(
'Tips untuk hasil terbaik:',
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('• Pastikan foto jelas dan tidak buram'),
Text('• Gunakan pencahayaan yang cukup'),
Text('• Atur preferensi dengan lengkap'),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Tutup'),
),
],
),
);
}
void _showAbout() {
showAboutDialog(
context: context,
applicationName: AppConstants.appName,
applicationVersion: AppConstants.appVersion,
applicationIcon: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppTheme.primaryBrown,
borderRadius: BorderRadius.circular(16),
),
child: const Icon(Icons.local_cafe, color: Colors.white, size: 32),
),
children: [
const Text('Aplikasi rekomendasi menu kafe menggunakan AI dan OCR'),
const SizedBox(height: 16),
Text('© 2024 ${AppConstants.kafeName}'),
],
);
}
Future<void> _signOut() async {
final confirmed = await DialogService().showConfirmDialog(
title: 'Keluar',
message: 'Apakah Anda yakin ingin keluar dari aplikasi?',
isDanger: true,
context: context,
);
if (confirmed == true) {
try {
await FirebaseService().signOut();
if (mounted) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const LoginScreen()),
(route) => false,
);
}
} catch (e) {
if (mounted) {
DialogService().showError('Gagal keluar: $e');
}
}
}
}
}