1396 lines
45 KiB
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();
|
|
}
|
|
}
|