amoriai/lib/screens/home/home_screen.dart

1396 lines
45 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:animated_bottom_navigation_bar/animated_bottom_navigation_bar.dart';
import '../../core/constants/app_constants.dart';
import '../../models/recommendation_model.dart';
import '../../models/user_model.dart';
import '../../services/firebase_service.dart';
import '../../services/daily_checkin_service.dart';
import '../camera/photo_picker_screen.dart';
import '../history/history_screen.dart';
import '../profile/profile_screen.dart';
import '../onboarding/daily_checkin_screen.dart';
import '../recommendation/recommendation_screen.dart';
import '../chatbot/amor_chatbot_screen.dart';
class HomeScreen extends ConsumerStatefulWidget {
const HomeScreen({super.key});
@override
ConsumerState<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends ConsumerState<HomeScreen>
with SingleTickerProviderStateMixin {
int _currentIndex = 0;
final PageController _pageController = PageController();
final GlobalKey<_HomeTabState> _homeTabKey = GlobalKey<_HomeTabState>();
late AnimationController _fabAnimController;
@override
void initState() {
super.initState();
_fabAnimController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
)..forward();
// Show daily check-in when app first opens (non-mandatory, user can skip)
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkDailyCheckin(isMandatory: false);
});
}
@override
void dispose() {
_pageController.dispose();
_fabAnimController.dispose();
super.dispose();
}
void _onTabTapped(int index) {
setState(() => _currentIndex = index);
_pageController.animateToPage(
index,
duration: AppConstants.shortAnimation,
curve: Curves.easeInOut,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
extendBody: true,
body: PageView(
controller: _pageController,
onPageChanged: (index) => setState(() => _currentIndex = index),
children: [
_HomeTab(key: _homeTabKey),
const _HistoryTab(),
const _FavoritesTab(),
const _ProfileTab(),
],
),
floatingActionButton: ScaleTransition(
scale: CurvedAnimation(
parent: _fabAnimController,
curve: Curves.elasticOut,
),
child: Container(
width: 62,
height: 62,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFD4A574), Color(0xFF8B5E3C), Color(0xFF6B3A2A)],
),
boxShadow: [
BoxShadow(
color: const Color(0xFF8B5E3C).withOpacity(0.45),
blurRadius: 16,
spreadRadius: 2,
offset: const Offset(0, 6),
),
BoxShadow(
color: const Color(0xFFD4A574).withOpacity(0.3),
blurRadius: 24,
offset: const Offset(0, 2),
),
],
),
child: FloatingActionButton(
onPressed: () => _handleScanMenu(context),
backgroundColor: Colors.transparent,
elevation: 0,
shape: const CircleBorder(),
child: const Icon(
Icons.document_scanner_rounded,
color: Colors.white,
size: 26,
),
),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: AnimatedBottomNavigationBar(
icons: const [
Icons.home_rounded,
Icons.history_rounded,
Icons.favorite_rounded,
Icons.person_rounded,
],
activeIndex: _currentIndex,
gapLocation: GapLocation.center,
notchSmoothness: NotchSmoothness.softEdge,
notchMargin: 8,
leftCornerRadius: 24,
rightCornerRadius: 24,
activeColor: const Color(0xFF6B3A2A),
inactiveColor: const Color(0xFFB0A090),
backgroundColor: Colors.white,
splashColor: const Color(0xFF6B3A2A).withOpacity(0.08),
splashSpeedInMilliseconds: 300,
elevation: 16,
shadow: Shadow(
color: Colors.black.withOpacity(0.08),
offset: const Offset(0, -4),
blurRadius: 20,
),
onTap: _onTabTapped,
),
);
}
Future<void> _handleScanMenu(BuildContext context) async {
final result = await _checkDailyCheckin(isMandatory: true);
if (result && mounted) _showImageSourceOptions(context);
}
Future<bool> _checkDailyCheckin({bool isMandatory = false}) async {
try {
final user = FirebaseService().currentUser;
if (user == null) return false;
final shouldShow = await DailyCheckinService().shouldShowCheckin(
user.uid,
);
// Already done today — skip the screen regardless of context
if (!shouldShow) return true;
if (mounted) {
final result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DailyCheckinScreen(isMandatory: isMandatory),
),
);
if (result == true) {
// Refresh recommendations list on the home tab
_homeTabKey.currentState?._loadLatestRecommendations();
return true;
}
return false;
}
return false;
} catch (e) {
debugPrint('Error checking daily check-in: $e');
return false;
}
}
void _showImageSourceOptions(BuildContext context) {
_openPhotoPicker(context);
}
void _openPhotoPicker(BuildContext context) async {
final nav = Navigator.of(context);
await nav.push(
MaterialPageRoute(builder: (_) => const PhotoPickerScreen()),
);
_homeTabKey.currentState?._loadLatestRecommendations();
}
}
// ─────────────────────────────────────────────
// HOME TAB
// ─────────────────────────────────────────────
class _HomeTab extends ConsumerStatefulWidget {
const _HomeTab({super.key});
@override
ConsumerState<_HomeTab> createState() => _HomeTabState();
}
class _HomeTabState extends ConsumerState<_HomeTab> {
List<RecommendationModel> _latestRecommendations = [];
bool _isLoadingRecommendations = true;
UserModel? _userData;
@override
void initState() {
super.initState();
_loadLatestRecommendations();
}
Future<void> _loadLatestRecommendations() async {
try {
final user = FirebaseService().currentUser;
if (user == null) {
if (mounted) setState(() => _isLoadingRecommendations = false);
return;
}
// Fetch a larger batch so after the 3-day filter we still have up to 10
final all = await FirebaseService().getUserRecommendations(
user.uid,
limit: 50,
);
final cutoff = DateTime.now().subtract(const Duration(days: 3));
final recent = all
.where((r) => r.createdAt.isAfter(cutoff) && r.selectedMenuName != null)
.take(10)
.toList();
// Fetch user data for nickname
final userData = await FirebaseService().getUserData(user.uid);
if (mounted) {
setState(() {
_latestRecommendations = recent;
_isLoadingRecommendations = false;
_userData = userData;
});
}
} catch (e) {
debugPrint('Error loading recommendations: $e');
if (mounted) setState(() => _isLoadingRecommendations = false);
}
}
Future<void> _clearLatestRecommendations(BuildContext context) async {
final confirm = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Text(
'Hapus Riwayat Scan?',
style: TextStyle(fontFamily: 'Poppins', fontWeight: FontWeight.bold),
),
content: const Text(
'Semua riwayat scan terbaru akan dihapus secara permanen. Lanjutkan?',
style: TextStyle(fontFamily: 'Poppins', fontSize: 13),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text(
'Batal',
style: TextStyle(fontFamily: 'Poppins', color: Colors.grey),
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade600,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: () => Navigator.pop(ctx, true),
child: const Text(
'Hapus',
style: TextStyle(fontFamily: 'Poppins', color: Colors.white),
),
),
],
),
);
if (confirm != true) return;
try {
for (final rec in _latestRecommendations) {
await FirebaseService().deleteRecommendation(rec.id);
}
if (mounted) {
setState(() => _latestRecommendations.clear());
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text(
'Riwayat scan berhasil dihapus',
style: TextStyle(fontFamily: 'Poppins'),
),
backgroundColor: Colors.green.shade600,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Gagal menghapus: $e',
style: const TextStyle(fontFamily: 'Poppins'),
),
backgroundColor: Colors.red.shade600,
behavior: SnackBarBehavior.floating,
),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F4EF),
body: RefreshIndicator(
onRefresh: _loadLatestRecommendations,
color: const Color(0xFF6B3A2A),
backgroundColor: Colors.white,
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
// Gradient Header
SliverToBoxAdapter(child: _buildGradientHeader(context)),
// Stats Row
SliverToBoxAdapter(child: _buildQuickStats()),
// Section Title
SliverToBoxAdapter(child: _buildSectionTitle(context)),
// Content
_isLoadingRecommendations
? SliverToBoxAdapter(child: _buildShimmerLoading())
: _latestRecommendations.isEmpty
? SliverToBoxAdapter(child: _buildEmptyState())
: _buildRecommendationsSliverList(),
// Bottom padding for FAB
const SliverToBoxAdapter(child: SizedBox(height: 100)),
],
),
),
);
}
Widget _buildGradientHeader(BuildContext context) {
final user = FirebaseService().currentUser;
// Gunakan nickname dari Firestore jika ada, jika tidak fallback ke displayName Auth atau default
final displayName = _userData?.nickname ?? user?.displayName ?? 'Pengguna';
final firstName = displayName.split(' ').first;
return Container(
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 20,
left: 24,
right: 24,
bottom: 28,
),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF3C2415), // deep espresso
Color(0xFF6B3A2A), // rich brown
Color(0xFF8B5E3C), // warm brown
],
stops: [0.0, 0.5, 1.0],
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(32),
bottomRight: Radius.circular(32),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Selamat Datang 👋',
style: TextStyle(
color: const Color(0xFFD4C4B0),
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
),
),
const SizedBox(height: 4),
Text(
firstName,
style: const TextStyle(
color: Colors.white,
fontSize: 26,
fontWeight: FontWeight.w800,
letterSpacing: -0.5,
),
),
],
),
),
_buildAvatar(user?.photoURL, firstName),
],
),
const SizedBox(height: 20),
// Action buttons row
Row(
children: [
Expanded(
child: _buildHeaderAction(
icon: Icons.document_scanner_outlined,
label: 'Scan Menu',
sublabel: 'Foto menu kafe',
onTap: () => _navigateToScan(),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildHeaderAction(
icon: Icons.smart_toy_outlined,
label: 'Tanya Seputar',
sublabel: 'Menu Amor Coffee',
onTap: () => _navigateToChat(),
),
),
],
),
],
),
);
}
void _navigateToScan() async {
await Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => const PhotoPickerScreen()));
_loadLatestRecommendations();
}
void _navigateToChat() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const AmorChatbotScreen(),
),
);
}
Widget _buildHeaderAction({
required IconData icon,
required String label,
required String sublabel,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.14),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withOpacity(0.18)),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFFD4A574).withOpacity(0.25),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: const Color(0xFFD4A574), size: 18),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
fontSize: 13,
),
),
Text(
sublabel,
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 11,
),
),
],
),
),
Icon(
Icons.arrow_forward_ios_rounded,
color: Colors.white.withOpacity(0.4),
size: 12,
),
],
),
),
);
}
Widget _buildAvatar(String? photoURL, String firstName) {
return Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(
colors: [Color(0xFFD4A574), Color(0xFFE8C9A8)],
),
boxShadow: [
BoxShadow(
color: const Color(0xFFD4A574).withOpacity(0.4),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Container(
width: 48,
height: 48,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Color(0xFF3C2415),
),
child: photoURL != null
? ClipOval(
child: Image.network(
photoURL,
fit: BoxFit.cover,
width: 48,
height: 48,
errorBuilder: (_, __, ___) => _buildAvatarText(firstName),
),
)
: _buildAvatarText(firstName),
),
);
}
Widget _buildAvatarText(String name) {
return Center(
child: Text(
name.isNotEmpty ? name[0].toUpperCase() : 'U',
style: const TextStyle(
color: Color(0xFFD4A574),
fontWeight: FontWeight.w800,
fontSize: 20,
),
),
);
}
Widget _buildQuickStats() {
final totalScans = _latestRecommendations.length;
final selectedCount = _latestRecommendations
.where((r) => r.selectedMenuName != null)
.length;
return Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
child: Row(
children: [
Expanded(
child: _buildStatCard(
icon: Icons.document_scanner_rounded,
value: '$totalScans',
label: 'Total Scan',
color: const Color(0xFF6B3A2A),
bgColor: const Color(0xFFFAF6F1),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
icon: Icons.check_circle_rounded,
value: '$selectedCount',
label: 'Menu Dipilih',
color: const Color(0xFF4CAF50),
bgColor: const Color(0xFFF1F8F2),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
icon: Icons.star_rounded,
value: _getAverageScore(),
label: 'Rata-rata',
color: const Color(0xFFE5A100),
bgColor: const Color(0xFFFFF8E8),
),
),
],
),
);
}
String _getAverageScore() {
if (_latestRecommendations.isEmpty) return '-';
double total = 0;
int count = 0;
for (var rec in _latestRecommendations) {
if (rec.recommendations.isNotEmpty) {
total += rec.topRecommendations.first.score * 100;
count++;
}
}
if (count == 0) return '-';
return '${(total / count).toStringAsFixed(0)}%';
}
Widget _buildStatCard({
required IconData icon,
required String value,
required String label,
required Color color,
required Color bgColor,
}) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(18),
border: Border.all(color: color.withOpacity(0.1)),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.06),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
Icon(icon, color: color, size: 22),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w800,
color: color,
),
),
const SizedBox(height: 2),
Text(
label,
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: Color(0xFF9E8E7E),
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildSectionTitle(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 12),
child: Row(
children: [
Container(
width: 4,
height: 22,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF8B5E3C), Color(0xFFD4A574)],
),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
const Expanded(
child: Text(
'Riwayat Scan Terbaru',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
color: Color(0xFF3C2415),
letterSpacing: -0.3,
),
),
),
if (_latestRecommendations.isNotEmpty) ...[
// Tombol hapus
GestureDetector(
onTap: () => _clearLatestRecommendations(context),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.delete_outline_rounded,
size: 14,
color: Colors.red.shade600,
),
const SizedBox(width: 4),
Text(
'Hapus',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.red.shade600,
),
),
],
),
),
),
const SizedBox(width: 8),
// Tombol lihat semua
GestureDetector(
onTap: () {
final homeState = context
.findAncestorStateOfType<_HomeScreenState>();
homeState?._onTabTapped(1);
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: const Color(0xFF6B3A2A).withOpacity(0.08),
borderRadius: BorderRadius.circular(20),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Semua',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF6B3A2A),
),
),
SizedBox(width: 2),
Icon(
Icons.arrow_forward_ios_rounded,
size: 10,
color: Color(0xFF6B3A2A),
),
],
),
),
),
],
],
),
);
}
Widget _buildShimmerLoading() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: List.generate(3, (i) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
height: 88,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
width: 56,
height: 56,
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF0EBE4),
borderRadius: BorderRadius.circular(14),
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 14,
width: 140,
decoration: BoxDecoration(
color: const Color(0xFFF0EBE4),
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 8),
Container(
height: 10,
width: 100,
decoration: BoxDecoration(
color: const Color(0xFFF0EBE4),
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
],
),
);
}),
),
);
}
Widget _buildEmptyState() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.symmetric(vertical: 48, horizontal: 24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: const Color(0xFF8B5E3C).withOpacity(0.05),
blurRadius: 20,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
// Decorative icon
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFF6B3A2A).withOpacity(0.08),
const Color(0xFFD4A574).withOpacity(0.12),
],
),
shape: BoxShape.circle,
),
child: const Center(
child: Text('📋', style: TextStyle(fontSize: 36)),
),
),
const SizedBox(height: 20),
const Text(
'Belum Ada Riwayat Scan',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
color: Color(0xFF3C2415),
),
),
const SizedBox(height: 8),
Text(
'Tekan tombol scan di bawah untuk\nmemulai menemukan menu terbaik!',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: const Color(0xFF9E8E7E),
height: 1.6,
),
),
const SizedBox(height: 24),
// Decorative dots
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
3,
(i) => Container(
width: 6,
height: 6,
margin: const EdgeInsets.symmetric(horizontal: 3),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFFD4A574).withOpacity(0.3 + i * 0.2),
),
),
),
),
],
),
);
}
SliverList _buildRecommendationsSliverList() {
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final rec = _latestRecommendations[index];
MenuRecommendation? selectedMenu;
if (rec.selectedMenuName != null) {
selectedMenu = rec.recommendations
.cast<MenuRecommendation?>()
.firstWhere(
(r) => r!.menuName == rec.selectedMenuName,
orElse: () => null,
);
}
selectedMenu ??= rec.recommendations.isNotEmpty
? rec.topRecommendations.first
: null;
if (selectedMenu == null) return const SizedBox.shrink();
// Wrap with Dismissible untuk swipe-to-delete
return Dismissible(
key: ValueKey(rec.id),
direction: DismissDirection.endToStart,
background: Container(
margin: EdgeInsets.only(
left: 20,
right: 20,
bottom: 10,
top: index == 0 ? 0 : 2,
),
decoration: BoxDecoration(
color: Colors.red.shade500,
borderRadius: BorderRadius.circular(20),
),
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.delete_rounded, color: Colors.white, size: 26),
SizedBox(height: 4),
Text(
'Hapus',
style: TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w600,
fontFamily: 'Poppins',
),
),
],
),
),
confirmDismiss: (_) async {
return await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: const Text(
'Hapus Riwayat Ini?',
style: TextStyle(
fontFamily: 'Poppins',
fontWeight: FontWeight.bold,
),
),
content: const Text(
'Riwayat scan ini akan dihapus secara permanen.',
style: TextStyle(fontFamily: 'Poppins', fontSize: 13),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text(
'Batal',
style: TextStyle(
fontFamily: 'Poppins',
color: Colors.grey,
),
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade600,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: () => Navigator.pop(ctx, true),
child: const Text(
'Hapus',
style: TextStyle(
fontFamily: 'Poppins',
color: Colors.white,
),
),
),
],
),
);
},
onDismissed: (_) => _deleteOneRecommendation(rec),
child: _buildPremiumCard(rec, selectedMenu, index),
);
}, childCount: _latestRecommendations.length),
);
}
Future<void> _deleteOneRecommendation(RecommendationModel rec) async {
try {
await FirebaseService().deleteRecommendation(rec.id);
if (mounted) {
setState(
() => _latestRecommendations.removeWhere((r) => r.id == rec.id),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text(
'Riwayat dihapus',
style: TextStyle(fontFamily: 'Poppins'),
),
backgroundColor: Colors.green.shade600,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
duration: const Duration(seconds: 2),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Gagal menghapus: $e',
style: const TextStyle(fontFamily: 'Poppins'),
),
backgroundColor: Colors.red.shade600,
behavior: SnackBarBehavior.floating,
),
);
// Reload agar state tidak kacau
_loadLatestRecommendations();
}
}
}
Widget _buildPremiumCard(
RecommendationModel recommendation,
MenuRecommendation menu,
int index,
) {
final isSelected = recommendation.selectedMenuName != null;
final score = (menu.score * 100).toInt();
final categoryIcon = _getCategoryIcon(menu.category);
return GestureDetector(
onTap: () {
Navigator.of(context)
.push(
MaterialPageRoute(
builder: (context) =>
RecommendationScreen(recommendation: recommendation),
),
)
.then((_) => _loadLatestRecommendations());
},
child: Container(
margin: EdgeInsets.only(
left: 20,
right: 20,
bottom: 10,
top: index == 0 ? 0 : 2,
),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected
? const Color(0xFFD4A574).withOpacity(0.3)
: const Color(0xFFF0EBE4),
),
boxShadow: [
BoxShadow(
color: isSelected
? const Color(0xFF8B5E3C).withOpacity(0.08)
: Colors.black.withOpacity(0.03),
blurRadius: isSelected ? 16 : 10,
offset: const Offset(0, 3),
),
],
),
child: Row(
children: [
// Icon with gradient
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isSelected
? [const Color(0xFF8B5E3C), const Color(0xFF6B3A2A)]
: [const Color(0xFFE8DFD4), const Color(0xFFD4CFC8)],
),
borderRadius: BorderRadius.circular(15),
boxShadow: isSelected
? [
BoxShadow(
color: const Color(0xFF8B5E3C).withOpacity(0.25),
blurRadius: 8,
offset: const Offset(0, 3),
),
]
: [],
),
child: Icon(
categoryIcon,
color: isSelected ? Colors.white : const Color(0xFF8B7B6B),
size: 24,
),
),
const SizedBox(width: 14),
// Content
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Menu name + badge
Row(
children: [
Expanded(
child: Text(
menu.menuName,
style: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 15,
color: Color(0xFF3C2415),
letterSpacing: -0.2,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
if (isSelected) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF43A047), Color(0xFF66BB6A)],
),
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'✓ Dipilih',
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w700,
color: Colors.white,
letterSpacing: 0.3,
),
),
),
],
],
),
const SizedBox(height: 6),
// Baris bawah: harga+skor kiri, tanggal kanan
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Kiri: harga · bintang skor
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
menu.formattedPrice,
style: const TextStyle(
color: Color(0xFF8B5E3C),
fontWeight: FontWeight.w700,
fontSize: 13,
),
),
Container(
width: 3,
height: 3,
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: const BoxDecoration(
color: Color(0xFFD4CFC8),
shape: BoxShape.circle,
),
),
Icon(
Icons.star_rounded,
size: 14,
color: score >= 80
? const Color(0xFFE5A100)
: const Color(0xFFBDB5AB),
),
const SizedBox(width: 2),
Text(
'$score%',
style: TextStyle(
fontSize: 12,
color: score >= 80
? const Color(0xFFE5A100)
: const Color(0xFF9E8E7E),
fontWeight: FontWeight.w600,
),
),
],
),
// Kanan: tanggal
const SizedBox(width: 8),
Flexible(
child: Text(
recommendation.formattedDate,
style: const TextStyle(
fontSize: 11,
color: Color(0xFFBDB5AB),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
),
),
],
),
],
),
),
const SizedBox(width: 6),
// Tombol hapus per item
GestureDetector(
onTap: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: const Text(
'Hapus Riwayat Ini?',
style: TextStyle(
fontFamily: 'Poppins',
fontWeight: FontWeight.bold,
),
),
content: const Text(
'Riwayat scan ini akan dihapus secara permanen.',
style: TextStyle(fontFamily: 'Poppins', fontSize: 13),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text(
'Batal',
style: TextStyle(
fontFamily: 'Poppins',
color: Colors.grey,
),
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade600,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: () => Navigator.pop(ctx, true),
child: const Text(
'Hapus',
style: TextStyle(
fontFamily: 'Poppins',
color: Colors.white,
),
),
),
],
),
);
if (confirm == true) _deleteOneRecommendation(recommendation);
},
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.delete_outline_rounded,
color: Colors.red.shade400,
size: 16,
),
),
),
const SizedBox(width: 6),
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: const Color(0xFFF8F4EF),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.chevron_right_rounded,
color: Color(0xFFBDB5AB),
size: 18,
),
),
],
),
),
);
}
IconData _getCategoryIcon(String category) {
final lower = category.toLowerCase();
if (lower.contains('coffee') || lower.contains('kopi')) return Icons.coffee;
if (lower.contains('tea') || lower.contains('teh')) {
return Icons.emoji_food_beverage;
}
if (lower.contains('food') || lower.contains('makanan')) {
return Icons.restaurant;
}
if (lower.contains('drink') || lower.contains('minuman')) {
return Icons.local_drink;
}
if (lower.contains('dessert') || lower.contains('snack')) return Icons.cake;
return Icons.local_cafe;
}
}
// ─────────────────────────────────────────────
// OTHER TABS
// ─────────────────────────────────────────────
class _HistoryTab extends ConsumerWidget {
const _HistoryTab();
@override
Widget build(BuildContext context, WidgetRef ref) {
return const HistoryScreen(initialIndex: 0);
}
}
class _FavoritesTab extends ConsumerWidget {
const _FavoritesTab();
@override
Widget build(BuildContext context, WidgetRef ref) {
return const HistoryScreen(initialIndex: 1);
}
}
class _ProfileTab extends ConsumerWidget {
const _ProfileTab();
@override
Widget build(BuildContext context, WidgetRef ref) {
return const ProfileScreen();
}
}