import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/theme/app_theme.dart'; import '../../core/constants/app_constants.dart'; import '../../models/recommendation_model.dart'; import '../../services/ocr_service.dart'; import '../../services/gemini_service.dart'; import '../../services/firebase_service.dart'; import 'package:flutter_animate/flutter_animate.dart'; import '../../services/dialog_service.dart'; class RecommendationScreen extends ConsumerStatefulWidget { final RecommendationModel recommendation; final OCRResult? ocrResult; const RecommendationScreen({ super.key, required this.recommendation, this.ocrResult, }); @override ConsumerState createState() => _RecommendationScreenState(); } class _RecommendationScreenState extends ConsumerState with SingleTickerProviderStateMixin { late TabController _tabController; final TextEditingController _chatController = TextEditingController(); final List _chatMessages = []; bool _isLoadingChat = false; String? _selectedMenuName; // Menu yang sedang dijadikan konteks di chat (Shopee-style) MenuRecommendation? _chatContextMenu; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); // Restore previously selected menu (if any) _selectedMenuName = widget.recommendation.selectedMenuName; // Generate a dynamic greeting based on recommendations final recommendationsText = widget.recommendation.recommendations .take(3) .map((r) => '${r.menuName} (${r.scorePercentage} cocok)') .join(', '); _chatMessages.add( ChatMessage( text: 'Halo! Berdasarkan menu yang saya analisis dan preferensi Anda, saya merekomendasikan: $recommendationsText.\n\nAda yang bisa saya bantu jelaskan lebih lanjut tentang menu-menu ini? Atau Anda punya kriteria lain?', isUser: false, ), ); } @override void dispose() { _tabController.dispose(); _chatController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return PopScope( canPop: false, onPopInvoked: (didPop) async { if (didPop) return; final shouldPop = await _showBackConfirmation(); if (shouldPop && mounted) { Navigator.of(context).popUntil((route) => route.isFirst); } }, child: Scaffold( backgroundColor: AppTheme.warmWhite, appBar: AppBar( title: const Text( 'Rekomendasi Menu', style: TextStyle( color: AppTheme.primaryBrown, fontWeight: FontWeight.bold, ), ), backgroundColor: AppTheme.warmWhite, elevation: 0, iconTheme: const IconThemeData(color: AppTheme.primaryBrown), actions: [ IconButton( icon: Icon( widget.recommendation.isFavorite ? Icons.favorite : Icons.favorite_border, color: widget.recommendation.isFavorite ? AppTheme.error : AppTheme.primaryBrown, ), onPressed: _toggleFavorite, ), PopupMenuButton( icon: const Icon(Icons.more_vert, color: AppTheme.primaryBrown), itemBuilder: (context) => [ const PopupMenuItem( value: 'share', child: Row( children: [ Icon(Icons.share), SizedBox(width: 8), Text('Bagikan'), ], ), ), const PopupMenuItem( value: 'save', child: Row( children: [ Icon(Icons.bookmark_border), SizedBox(width: 8), Text('Simpan'), ], ), ), ], onSelected: (value) { if (value == 'share') { _shareRecommendation(); } else if (value == 'save') { _saveRecommendation(); } }, ), ], bottom: TabBar( controller: _tabController, labelColor: AppTheme.primaryBrown, unselectedLabelColor: AppTheme.grey500, indicatorColor: AppTheme.primaryBrown, tabs: const [ Tab(text: 'Rekomendasi'), Tab(text: 'Menu Asli'), Tab(text: 'Chat AI'), ], ), ), body: TabBarView( controller: _tabController, children: [ _buildRecommendationTab(), _buildOriginalMenuTab(), _buildChatTab(), ], ), ), ); } Widget _buildRecommendationTab() { final recs = widget.recommendation.recommendations; final keinginan = widget.recommendation.recommendations.isNotEmpty ? _detectKeinginan(recs) : 'minuman'; return SingleChildScrollView( padding: const EdgeInsets.all(AppConstants.defaultPadding), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Card _buildHeaderCard(), const SizedBox(height: 24), // Recommendations List Text( 'Rekomendasi untuk Anda', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, color: AppTheme.primaryBrown, ), ), const SizedBox(height: 4), // Subtitle hint Text( keinginan == 'keduanya' ? 'Slot 1 & 2 adalah pasangan menu yang saling melengkapi ☕🍽️' : keinginan == 'minuman' ? '3 pilihan minuman terbaik untuk Anda 🥤' : '3 pilihan makanan terbaik untuk Anda 🍽️', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppTheme.grey600, fontStyle: FontStyle.italic, ), ), const SizedBox(height: 16), ...List.generate(recs.length > 3 ? 3 : recs.length, (index) { final rec = recs[index]; final slotLabels = keinginan == 'keduanya' ? ['Paket Utama ⭐', 'Kombinasi Alternatif 🔄', 'Paket Hemat 💰'] : keinginan == 'minuman' ? ['Pilihan Terbaik ⭐', 'Alternatif 🔄', 'Pilihan Hemat 💰'] : ['Pilihan Terbaik ⭐', 'Alternatif 🔄', 'Pilihan Hemat 💰']; final slotLabel = index < slotLabels.length ? slotLabels[index] : 'Slot ${index + 1}'; return _buildSlottedRecommendationCard(rec, index + 1, slotLabel, keinginan); }), ], ), ); } /// Deteksi jenis keinginan dari distribusi rekomendasi String _detectKeinginan(List recs) { int drinkCount = 0; int foodCount = 0; for (final rec in recs) { final cat = rec.category.toLowerCase(); final name = rec.menuName.toLowerCase(); final isDrink = cat.contains('minuman') || cat.contains('kopi') || cat.contains('teh') || cat.contains('drink') || name.contains('kopi') || name.contains('coffee') || name.contains('teh') || name.contains('latte') || name.contains('jus') || name.contains('milk') || name.contains('matcha') || name.contains('mocha'); if (isDrink) drinkCount++; else foodCount++; } if (drinkCount > 0 && foodCount > 0) return 'keduanya'; if (drinkCount > foodCount) return 'minuman'; return 'makanan'; } Widget _buildSlottedRecommendationCard( MenuRecommendation recommendation, int slotNumber, String slotLabel, String keinginan, ) { // ── Mapping Type langsung dari AI (Lebih Akurat) ── final String type = recommendation.slotType?.toLowerCase() ?? ''; final bool isMakanan = type.contains('makan'); final bool isMinuman = type.contains('minum'); final bool isBundling = type.contains('bundling'); // Tampilkan badge tipe menu secara eksplisit jika AI memberikannya final bool showTypeBadge = isMakanan || isMinuman || isBundling; final isSelectedMenu = _selectedMenuName == recommendation.menuName; return Column( children: [ // Slot Header Label Padding( padding: const EdgeInsets.only(bottom: 6), child: Row( children: [ Container( width: 28, height: 28, decoration: BoxDecoration( color: AppTheme.primaryBrown, shape: BoxShape.circle, ), child: Center( child: Text( '$slotNumber', style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold, ), ), ), ), const SizedBox(width: 8), Text( slotLabel, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppTheme.primaryBrown, ), ), if (showTypeBadge) ...[ const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: isBundling ? Colors.green.shade50 : (isMakanan ? Colors.orange.shade50 : Colors.blue.shade50), border: Border.all(color: isBundling ? Colors.green.shade300 : (isMakanan ? Colors.orange.shade300 : Colors.blue.shade300)), borderRadius: BorderRadius.circular(8), ), child: Text( isBundling ? '🍱 Paket Bundling' : (isMakanan ? '🍽️ Makanan' : '☕ Minuman'), style: TextStyle( fontSize: 11, color: isBundling ? Colors.green.shade700 : (isMakanan ? Colors.orange.shade700 : Colors.blue.shade700), fontWeight: FontWeight.w500, ), ), ), ], ], ), ), // The actual recommendation card _buildRecommendationCard(recommendation), ], ); } Widget _buildHeaderCard() { return Container( width: double.infinity, padding: const EdgeInsets.all(20), 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( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.auto_awesome, color: Colors.white, size: 24) .animate(onPlay: (controller) => controller.repeat()) .shimmer( duration: 2000.ms, color: Colors.white.withOpacity(0.5), ), const SizedBox(width: 8), Expanded( child: Text( 'Rekomendasi Personal', style: Theme.of(context).textTheme.titleLarge?.copyWith( color: Colors.white, fontWeight: FontWeight.bold, ), ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 4, ), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: Text( '${(widget.recommendation.confidence * 100).toInt()}% Match', style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox(height: 12), Text( 'Berdasarkan ${widget.recommendation.recommendations.length} menu yang terdeteksi', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.white.withOpacity(0.9), ), ), const SizedBox(height: 8), Text( widget.recommendation.formattedDate, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.white.withOpacity(0.7), ), ), ], ), ).animate().scale(duration: 600.ms, curve: Curves.easeOutBack); } Widget _buildRecommendationCard(MenuRecommendation recommendation) { return Container( margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all( color: _selectedMenuName == recommendation.menuName ? Colors.green.shade400 : AppTheme.primaryBrown.withOpacity(0.08), width: _selectedMenuName == recommendation.menuName ? 2 : 1, ), boxShadow: [ BoxShadow( color: _selectedMenuName == recommendation.menuName ? Colors.green.withOpacity(0.12) : AppTheme.primaryBrown.withOpacity(0.06), blurRadius: 16, offset: const Offset(0, 6), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Title Header with gradient accent Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ AppTheme.primaryBrown.withOpacity(0.08), AppTheme.primaryBrown.withOpacity(0.02), ], ), borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), ), ), child: Row( children: [ // Coffee icon Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppTheme.primaryBrown.withOpacity(0.12), borderRadius: BorderRadius.circular(10), ), child: Icon( _getCategoryIcon(recommendation.category), color: AppTheme.primaryBrown, size: 20, ), ), const SizedBox(width: 12), // Title & Price Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( recommendation.menuName, style: Theme.of(context).textTheme.titleMedium ?.copyWith( fontWeight: FontWeight.bold, color: AppTheme.primaryBrown, letterSpacing: -0.3, ), ), const SizedBox(height: 2), Text( recommendation.formattedPrice, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppTheme.coffeeBrown, fontWeight: FontWeight.w600, ), ), ], ), ), // Score badge Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 6, ), decoration: BoxDecoration( gradient: const LinearGradient( colors: [AppTheme.primaryBrown, AppTheme.coffeeBrown], ), borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.star_rounded, color: Colors.amber, size: 14, ), const SizedBox(width: 3), Text( recommendation.scorePercentage, style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold, ), ), ], ), ), ], ), ), // Body content Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Star rating row Row( children: [ ...List.generate(5, (index) { return Padding( padding: const EdgeInsets.only(right: 2), child: Icon( index < recommendation.starRating ? Icons.star_rounded : Icons.star_outline_rounded, color: index < recommendation.starRating ? Colors.amber : AppTheme.grey400, size: 18, ), ); }), const SizedBox(width: 8), Text( 'Kesesuaian ${recommendation.scorePercentage}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppTheme.grey600, fontWeight: FontWeight.w500, ), ), ], ), const SizedBox(height: 12), // Tags Wrap( spacing: 6, runSpacing: 6, children: [ _buildTag(recommendation.category, AppTheme.primaryBrown), ...recommendation.tags.map( (tag) => _buildTag(tag, AppTheme.grey600), ), ], ), // Description if (recommendation.description.isNotEmpty) ...[ const SizedBox(height: 12), Text( recommendation.description, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppTheme.grey700, height: 1.5, ), ), ], const SizedBox(height: 12), // Personalized reason Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ AppTheme.primaryBrown.withOpacity(0.06), AppTheme.accent.withOpacity(0.04), ], ), borderRadius: BorderRadius.circular(12), border: Border.all( color: AppTheme.primaryBrown.withOpacity(0.08), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: AppTheme.primaryBrown.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: Icon( Icons.auto_awesome, size: 14, color: AppTheme.primaryBrown, ), ), const SizedBox(width: 6), Text( 'Mengapa cocok untuk Anda', style: Theme.of(context).textTheme.bodySmall ?.copyWith( color: AppTheme.primaryBrown, fontWeight: FontWeight.w600, ), ), ], ), const SizedBox(height: 6), Text( recommendation.reason, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppTheme.grey700, height: 1.4, ), ), ], ), ), const SizedBox(height: 14), // Action Buttons — Pilih Menu + Tanya AI Row( children: [ // Pilih Menu Expanded( child: ElevatedButton( onPressed: () => _orderItem(recommendation), style: ElevatedButton.styleFrom( backgroundColor: _selectedMenuName == recommendation.menuName ? Colors.green.shade600 : AppTheme.primaryBrown, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 0, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( _selectedMenuName == recommendation.menuName ? Icons.check_circle_outline : Icons.shopping_cart_outlined, size: 16, ), const SizedBox(width: 6), Text( _selectedMenuName == recommendation.menuName ? 'Dipilih ✓' : 'Pilih Menu', style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 13, ), ), ], ), ), ), const SizedBox(width: 10), // Tanya AI tentang menu ini Expanded( child: OutlinedButton( onPressed: () => _askAboutMenu(recommendation), style: OutlinedButton.styleFrom( foregroundColor: AppTheme.primaryBrown, side: BorderSide( color: AppTheme.primaryBrown.withOpacity(0.6), width: 1.5, ), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.chat_bubble_outline_rounded, size: 16), SizedBox(width: 6), Text( 'Tanya AI', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 13, ), ), ], ), ), ), ], ), ], ), ), ], ), ); } 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; } Widget _buildTag(String text, Color color) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withOpacity(0.3)), ), child: Text( text, style: TextStyle( color: color, fontSize: 12, fontWeight: FontWeight.w500, ), ), ); } /// Bangun section foto menu. Mendukung path lokal (Image.file) dan URL (Image.network). /// Jika ada menuImagePaths (multi-foto), tampilkan sebagai carousel horizontal. List _buildPhotoSection(String? fallbackUrl) { final rec = widget.recommendation; final List paths = []; if (rec.menuImagePaths != null && rec.menuImagePaths!.isNotEmpty) { paths.addAll(rec.menuImagePaths!); } else if (fallbackUrl != null && fallbackUrl.isNotEmpty) { paths.add(fallbackUrl); } if (paths.isEmpty) return []; final pageController = PageController(); return [ Text( paths.length > 1 ? 'Foto Menu (${paths.length})' : 'Foto Menu', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w800, color: const Color(0xFF3C2415), letterSpacing: -0.3, ), ), const SizedBox(height: 12), // Hint tap to expand Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(16), child: SizedBox( height: 220, child: PageView.builder( controller: pageController, itemCount: paths.length, itemBuilder: (context, index) { return GestureDetector( onTap: () => _openFullscreen(paths, index), child: _buildSinglePhotoWidget(paths[index]), ); }, ), ), ), // Expand icon overlay (top-right) Positioned( top: 8, right: 8, child: GestureDetector( onTap: () => _openFullscreen(paths, 0), child: Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Colors.black.withOpacity(0.45), borderRadius: BorderRadius.circular(8), ), child: const Icon( Icons.fullscreen_rounded, color: Colors.white, size: 18, ), ), ), ), ], ), if (paths.length > 1) ...[ const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(paths.length, (i) { return AnimatedBuilder( animation: pageController, builder: (context, _) { final page = pageController.hasClients ? (pageController.page ?? 0).round() : 0; final isActive = page == i; return Container( margin: const EdgeInsets.symmetric(horizontal: 3), width: isActive ? 18 : 6, height: 6, decoration: BoxDecoration( color: isActive ? const Color(0xFF8B5E3C) : const Color(0xFFD4CFC8), borderRadius: BorderRadius.circular(3), ), ); }, ); }), ), ], const SizedBox(height: 24), ]; } void _openFullscreen(List paths, int initialIndex) { Navigator.of(context).push( PageRouteBuilder( opaque: false, barrierColor: Colors.black, pageBuilder: (_, __, ___) => _FullscreenPhotoViewer(paths: paths, initialIndex: initialIndex), transitionsBuilder: (_, animation, __, child) => FadeTransition(opacity: animation, child: child), transitionDuration: const Duration(milliseconds: 220), ), ); } Widget _buildSinglePhotoWidget(String pathOrUrl) { // Deteksi apakah ini path lokal atau URL final isLocalFile = !pathOrUrl.startsWith('http'); final errorWidget = Container( height: 220, decoration: const BoxDecoration(color: Color(0xFFF0EBE4)), child: const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.image_not_supported_outlined, color: Color(0xFFBDB5AB), size: 32, ), SizedBox(height: 6), Text( 'Gambar tidak tersedia', style: TextStyle(color: Color(0xFFBDB5AB), fontSize: 12), ), ], ), ), ); if (isLocalFile) { final file = File(pathOrUrl); return file.existsSync() ? Image.file( file, width: double.infinity, height: 220, fit: BoxFit.cover, ) : errorWidget; } else { return Image.network( pathOrUrl, width: double.infinity, height: 220, fit: BoxFit.cover, errorBuilder: (_, __, ___) => errorWidget, ); } } Widget _buildOriginalMenuTab() { final menuItems = widget.recommendation.recommendations; final menuImageUrl = widget.recommendation.menuImageUrl; return SingleChildScrollView( padding: const EdgeInsets.all(AppConstants.defaultPadding), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── Foto Menu Section ──────────────────────────────────────── ..._buildPhotoSection(menuImageUrl), // Stats Bar Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFFFAF6F1), Color(0xFFF5EDE3)], ), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFE8DFD4)), ), child: Row( children: [ const Icon( Icons.restaurant_menu_rounded, color: Color(0xFF8B5E3C), size: 20, ), const SizedBox(width: 8), Text( '${menuItems.length} menu terdeteksi', style: const TextStyle( color: Color(0xFF6B3A2A), fontWeight: FontWeight.w700, fontSize: 14, ), ), const Spacer(), if (widget.ocrResult != null) Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( color: const Color(0xFF6B3A2A).withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Text( 'OCR ${((widget.ocrResult?.confidence ?? 0) * 100).toStringAsFixed(0)}%', style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF6B3A2A), ), ), ), ], ), ), const SizedBox(height: 20), // Section Title Row( children: [ Container( width: 4, height: 20, decoration: BoxDecoration( gradient: const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFF8B5E3C), Color(0xFFD4A574)], ), borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 8), Text( 'Daftar Menu', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w800, color: const Color(0xFF3C2415), letterSpacing: -0.3, ), ), ], ), const SizedBox(height: 14), // Menu Items List if (menuItems.isEmpty) _buildEmptyMenuState() else ...menuItems.asMap().entries.map( (entry) => _buildMenuDetailCard(entry.value, entry.key + 1), ), const SizedBox(height: 20), // Raw Text Section (collapsible) if (widget.recommendation.originalMenuText.isNotEmpty) Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFF0EBE4)), ), child: Theme( data: Theme.of( context, ).copyWith(dividerColor: Colors.transparent), child: ExpansionTile( tilePadding: const EdgeInsets.symmetric(horizontal: 16), childrenPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), leading: const Icon( Icons.text_snippet_outlined, color: Color(0xFF9E8E7E), size: 20, ), title: const Text( 'Teks Menu Asli', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF6B3A2A), ), ), children: [ Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFFFAF6F1), borderRadius: BorderRadius.circular(10), ), child: Text( widget.recommendation.originalMenuText, style: const TextStyle( fontFamily: 'monospace', fontSize: 12, color: Color(0xFF6B5B4B), height: 1.6, ), ), ), ], ), ), ), ], ), ); } Widget _buildEmptyMenuState() { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0xFFF0EBE4)), ), child: const Column( children: [ Text('🍽️', style: TextStyle(fontSize: 36)), SizedBox(height: 12), Text( 'Tidak ada menu terdeteksi', style: TextStyle( fontWeight: FontWeight.w600, color: Color(0xFF6B3A2A), fontSize: 15, ), ), SizedBox(height: 4), Text( 'Coba scan ulang dengan foto yang lebih jelas', style: TextStyle(color: Color(0xFF9E8E7E), fontSize: 12), ), ], ), ); } Widget _buildMenuDetailCard(MenuRecommendation item, int index) { final score = (item.score * 100).toInt(); final isSelected = widget.recommendation.selectedMenuName == item.menuName; return Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(18), border: Border.all( color: isSelected ? const Color(0xFFD4A574).withOpacity(0.4) : const Color(0xFFF0EBE4), ), boxShadow: [ BoxShadow( color: isSelected ? const Color(0xFF8B5E3C).withOpacity(0.08) : Colors.black.withOpacity(0.03), blurRadius: 10, offset: const Offset(0, 3), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header row with number, name, price Container( padding: const EdgeInsets.fromLTRB(16, 14, 16, 12), decoration: BoxDecoration( gradient: LinearGradient( colors: isSelected ? [ const Color(0xFF6B3A2A).withOpacity(0.06), const Color(0xFFD4A574).withOpacity(0.08), ] : [ const Color(0xFFFAF6F1), const Color(0xFFFAF6F1).withOpacity(0.5), ], ), borderRadius: const BorderRadius.only( topLeft: Radius.circular(18), topRight: Radius.circular(18), ), ), child: Row( children: [ // Number badge Container( width: 30, height: 30, decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF8B5E3C), Color(0xFF6B3A2A)], ), borderRadius: BorderRadius.circular(9), ), child: Center( child: Text( '$index', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w800, fontSize: 13, ), ), ), ), const SizedBox(width: 12), // Name Expanded( child: Text( item.menuName, style: const TextStyle( fontWeight: FontWeight.w700, fontSize: 16, color: Color(0xFF3C2415), letterSpacing: -0.3, ), ), ), // Price Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), decoration: BoxDecoration( color: const Color(0xFF6B3A2A).withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Text( item.formattedPrice, style: const TextStyle( color: Color(0xFF6B3A2A), fontWeight: FontWeight.w800, fontSize: 14, ), ), ), ], ), ), // Details body Padding( padding: const EdgeInsets.fromLTRB(16, 10, 16, 14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Category + Score Row Row( children: [ _buildInfoChip( icon: _getCategoryIcon(item.category), label: item.category, color: const Color(0xFF8B5E3C), ), const SizedBox(width: 8), _buildInfoChip( icon: Icons.star_rounded, label: '$score%', color: score >= 80 ? const Color(0xFFE5A100) : const Color(0xFF9E8E7E), ), if (isSelected) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF43A047), Color(0xFF66BB6A)], ), borderRadius: BorderRadius.circular(8), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.check_circle, color: Colors.white, size: 12, ), SizedBox(width: 3), Text( 'Dipilih', style: TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.w700, ), ), ], ), ), ], ], ), // Description if (item.description.isNotEmpty) ...[ const SizedBox(height: 10), Text( item.description, style: const TextStyle( color: Color(0xFF7B6B5B), fontSize: 13, height: 1.5, ), maxLines: 3, overflow: TextOverflow.ellipsis, ), ], // Tags if (item.tags.isNotEmpty) ...[ const SizedBox(height: 10), Wrap( spacing: 6, runSpacing: 6, children: item.tags .map( (tag) => Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: const Color(0xFFF5EDE3), borderRadius: BorderRadius.circular(6), ), child: Text( tag, style: const TextStyle( fontSize: 11, color: Color(0xFF8B7B6B), fontWeight: FontWeight.w500, ), ), ), ) .toList(), ), ], ], ), ), ], ), ); } Widget _buildInfoChip({ required IconData icon, required String label, required Color color, }) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 13, color: color), const SizedBox(width: 4), Text( label, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: color, ), ), ], ), ); } Widget _buildChatTab() { return Column( children: [ // Chat Messages Expanded( child: _chatMessages.isEmpty ? _buildChatPlaceholder() : ListView.builder( padding: const EdgeInsets.all(AppConstants.defaultPadding), itemCount: _chatMessages.length, itemBuilder: (context, index) { return _buildChatMessage(_chatMessages[index]); }, ), ), // Chat Input (includes context banner if _chatContextMenu != null) _buildChatInput(), ], ); } Widget _buildChatPlaceholder() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.chat_bubble_outline, size: 64, color: AppTheme.grey400), const SizedBox(height: 16), Text( 'Halo! Senang sekali bisa membantu Anda memilih menu di Amori Kofie Kediri.', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: AppTheme.grey800, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 8), Text( 'Ajukan pertanyaan tentang menu, alergi, atau preferensi lainnya', style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(color: AppTheme.grey500), textAlign: TextAlign.center, ), ], ), ); } Widget _buildChatMessage(ChatMessage message) { final isUser = message.isUser; return Container( margin: const EdgeInsets.only(bottom: 16), child: Row( mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isUser) ...[ CircleAvatar( radius: 16, backgroundColor: AppTheme.primaryBrown, child: const Icon(Icons.smart_toy, size: 16, color: Colors.white), ), const SizedBox(width: 8), ], Flexible( child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isUser ? AppTheme.primaryBrown : Colors.white, borderRadius: BorderRadius.circular(16).copyWith( bottomLeft: isUser ? const Radius.circular(16) : const Radius.circular(4), bottomRight: isUser ? const Radius.circular(4) : const Radius.circular(16), ), border: isUser ? null : Border.all(color: AppTheme.grey300, width: 1), boxShadow: isUser ? null : [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Text( message.text, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: isUser ? Colors.white : Colors.black87, fontSize: 14, fontWeight: FontWeight.w400, ), ), ), ), if (isUser) ...[ const SizedBox(width: 8), CircleAvatar( radius: 16, backgroundColor: AppTheme.grey300, child: const Icon(Icons.person, size: 16, color: Colors.white), ), ], ], ), ).animate().fade().slideY(begin: 0.2, end: 0, curve: Curves.easeOut); } Widget _buildChatInput() { return Container( color: Colors.white, child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── Shopee-style context banner ─────────────────────────────── if (_chatContextMenu != null) _buildChatContextBanner(_chatContextMenu!), // ── Input row ───────────────────────────────────────────────── Container( padding: const EdgeInsets.all(AppConstants.defaultPadding), decoration: BoxDecoration( color: Colors.white, boxShadow: _chatContextMenu == null ? [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, -2), ), ] : [], ), child: Row( children: [ Expanded( child: TextField( controller: _chatController, style: const TextStyle(color: Colors.black, fontSize: 16), decoration: InputDecoration( hintText: _chatContextMenu != null ? 'Tanya tentang ${_chatContextMenu!.menuName}...' : 'Tanya tentang menu...', hintStyle: TextStyle( color: AppTheme.grey500, fontSize: 14, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none, ), filled: true, fillColor: AppTheme.grey50, contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), ), maxLines: null, textCapitalization: TextCapitalization.sentences, autofocus: _chatContextMenu != null, ), ), const SizedBox(width: 8), Container( decoration: const BoxDecoration( color: AppTheme.primaryBrown, shape: BoxShape.circle, ), child: IconButton( onPressed: _isLoadingChat ? null : _sendMessage, icon: _isLoadingChat ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( Colors.white, ), ), ) : const Icon(Icons.send, color: Colors.white), ), ), ], ), ), ], ), ); } /// Banner konteks menu — seperti produk yang diklik di Shopee chat Widget _buildChatContextBanner(MenuRecommendation menu) { return Container( margin: const EdgeInsets.fromLTRB(12, 6, 12, 0), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: AppTheme.primaryBrown.withOpacity(0.06), borderRadius: BorderRadius.circular(14), border: Border.all(color: AppTheme.primaryBrown.withOpacity(0.2)), ), child: Row( children: [ // Ikon kategori Container( width: 40, height: 40, decoration: BoxDecoration( color: AppTheme.primaryBrown.withOpacity(0.12), borderRadius: BorderRadius.circular(10), ), child: Icon( _getCategoryIcon(menu.category), color: AppTheme.primaryBrown, size: 20, ), ), const SizedBox(width: 10), // Info menu Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( menu.menuName, style: const TextStyle( fontFamily: 'Poppins', fontWeight: FontWeight.w700, fontSize: 13, color: Color(0xFF3C2415), ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Row( children: [ Text( menu.formattedPrice, style: TextStyle( fontFamily: 'Poppins', fontSize: 11, fontWeight: FontWeight.w600, color: AppTheme.primaryBrown, ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: AppTheme.primaryBrown.withOpacity(0.1), borderRadius: BorderRadius.circular(4), ), child: Text( menu.scorePercentage, style: TextStyle( fontFamily: 'Poppins', fontSize: 10, color: AppTheme.primaryBrown, fontWeight: FontWeight.w600, ), ), ), ], ), ], ), ), // Tombol dismiss GestureDetector( onTap: () => setState(() => _chatContextMenu = null), child: Container( padding: const EdgeInsets.all(4), child: Icon( Icons.close_rounded, size: 16, color: AppTheme.primaryBrown.withOpacity(0.6), ), ), ), ], ), ); } /// Tanya AI tentang menu tertentu — Shopee-style: /// Tidak auto-send, cukup set konteks menu dan pindah ke tab Chat AI. void _askAboutMenu(MenuRecommendation recommendation) { setState(() => _chatContextMenu = recommendation); // Pindah ke tab Chat AI (index 2) _tabController.animateTo(2); } Future _sendMessage() async { final message = _chatController.text.trim(); if (message.isEmpty) return; // Jika ada konteks menu, sertakan di pesan final contextNote = _chatContextMenu != null ? ' [Konteks: ${_chatContextMenu!.menuName} – ${_chatContextMenu!.formattedPrice}]' : ''; final fullMessage = message + contextNote; setState(() { // Tampilkan pesan user tanpa note konteks _chatMessages.add(ChatMessage(text: message, isUser: true)); _isLoadingChat = true; // Hapus konteks setelah dikirim (seperti Shopee) _chatContextMenu = null; }); _chatController.clear(); try { final response = await GeminiService().chatWithAI( message: fullMessage, // Kirim pesan lengkap dengan konteks menu ke AI context: widget.recommendation, conversationHistory: _chatMessages .where((msg) => !msg.isUser) .map((msg) => msg.text) .toList(), ); setState(() { _chatMessages.add(ChatMessage(text: response, isUser: false)); }); } catch (e) { setState(() { _chatMessages.add( ChatMessage( text: 'Maaf, terjadi kesalahan. Silakan coba lagi.', isUser: false, ), ); }); } finally { setState(() => _isLoadingChat = false); } } Future _toggleFavorite() async { try { final newFavoriteStatus = !widget.recommendation.isFavorite; await FirebaseService().toggleRecommendationFavorite( widget.recommendation.id, newFavoriteStatus, ); setState(() { widget.recommendation.copyWith(isFavorite: newFavoriteStatus); }); DialogService().showSuccess( newFavoriteStatus ? 'Ditambahkan ke favorit' : 'Dihapus dari favorit', ); } catch (e) { DialogService().showError('Gagal mengupdate favorit: $e'); } } void _shareRecommendation() { // Implement share functionality DialogService().showInfo('Fitur berbagi akan segera hadir'); } void _saveRecommendation() { DialogService().showSuccess('Rekomendasi telah disimpan'); } void _orderItem(MenuRecommendation recommendation) async { try { // Toggle selection: tap again to deselect final isAlreadySelected = _selectedMenuName == recommendation.menuName; final newName = isAlreadySelected ? null : recommendation.menuName; setState(() => _selectedMenuName = newName); // Update the recommendation model with selected menu widget.recommendation.selectedMenuName = newName; // Save to Firebase await FirebaseService().saveRecommendation(widget.recommendation); if (mounted) { if (isAlreadySelected) { DialogService().showInfo('Pilihan dibatalkan.'); } else { DialogService().showSuccess( '${recommendation.menuName} dipilih! Tunjukkan ke kasir untuk memesan.', ); } } } catch (e) { debugPrint('Error saving selected menu: $e'); if (mounted) { DialogService().showError('Gagal menyimpan pilihan: $e'); } } } Future _showBackConfirmation() async { final bool isSelected = _selectedMenuName != null; final String title = isSelected ? 'Kembali ke Beranda?' : 'Batalkan Rekomendasi?'; final String content = isSelected ? 'Menu telah dipilih. Rekomendasi ini sudah otomatis tersimpan di riwayat Beranda Anda.' : 'Anda belum memilih menu rekomendasi satupun. Jika Anda kembali, hasil rekomendasi ini hanya akan tersimpan sebagai draf di Riwayat Scan Menu, namun tidak akan muncul di Riwayat Beranda. Yakin ingin kembali?'; final result = await showDialog( context: context, builder: (context) => AlertDialog( title: Text( title, style: const TextStyle(fontWeight: FontWeight.bold, color: AppTheme.primaryBrown), ), content: Text( content, style: const TextStyle(height: 1.4, color: AppTheme.grey800), ), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Batal', style: TextStyle(color: AppTheme.grey600)), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), style: ElevatedButton.styleFrom( backgroundColor: isSelected ? AppTheme.primaryBrown : AppTheme.error, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), child: Text(isSelected ? 'Ya, Kembali' : 'Ya, Batalkan'), ), ], ), ); return result ?? false; } } class ChatMessage { final String text; final bool isUser; final DateTime timestamp; ChatMessage({required this.text, required this.isUser, DateTime? timestamp}) : timestamp = timestamp ?? DateTime.now(); } /// ───────────────────────────────────────────────────────────────────────────── /// Fullscreen photo viewer — swipe antar foto, pinch-to-zoom, tap to close /// ───────────────────────────────────────────────────────────────────────────── class _FullscreenPhotoViewer extends StatefulWidget { final List paths; final int initialIndex; const _FullscreenPhotoViewer({ required this.paths, required this.initialIndex, }); @override State<_FullscreenPhotoViewer> createState() => _FullscreenPhotoViewerState(); } class _FullscreenPhotoViewerState extends State<_FullscreenPhotoViewer> { late PageController _pageController; late int _currentIndex; @override void initState() { super.initState(); _currentIndex = widget.initialIndex; _pageController = PageController(initialPage: widget.initialIndex); } @override void dispose() { _pageController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: Stack( children: [ // ── Foto carousel ───────────────────────────────────────────── PageView.builder( controller: _pageController, itemCount: widget.paths.length, onPageChanged: (i) => setState(() => _currentIndex = i), itemBuilder: (ctx, index) { return GestureDetector( onTap: () => Navigator.of(context).pop(), child: Center( child: InteractiveViewer( minScale: 0.8, maxScale: 5.0, child: _buildPhoto(widget.paths[index]), ), ), ); }, ), // ── Top bar: close button + counter ────────────────────────── Positioned( top: 0, left: 0, right: 0, child: SafeArea( child: Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Counter badge if (widget.paths.length > 1) Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( color: Colors.black.withOpacity(0.55), borderRadius: BorderRadius.circular(20), ), child: Text( '${_currentIndex + 1} / ${widget.paths.length}', style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600, fontFamily: 'Poppins', ), ), ) else const SizedBox.shrink(), // Close button GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( width: 36, height: 36, decoration: BoxDecoration( color: Colors.black.withOpacity(0.55), shape: BoxShape.circle, ), child: const Icon( Icons.close_rounded, color: Colors.white, size: 20, ), ), ), ], ), ), ), ), // ── Dot indicators (bottom) ──────────────────────────────── if (widget.paths.length > 1) Positioned( bottom: 32, left: 0, right: 0, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(widget.paths.length, (i) { final active = i == _currentIndex; return AnimatedContainer( duration: const Duration(milliseconds: 250), margin: const EdgeInsets.symmetric(horizontal: 4), width: active ? 20 : 7, height: 7, decoration: BoxDecoration( color: active ? Colors.white : Colors.white.withOpacity(0.4), borderRadius: BorderRadius.circular(4), ), ); }), ), ), ], ), ); } Widget _buildPhoto(String pathOrUrl) { final isLocal = !pathOrUrl.startsWith('http'); final error = const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.broken_image_outlined, color: Colors.white38, size: 48), SizedBox(height: 8), Text( 'Gambar tidak tersedia', style: TextStyle(color: Colors.white38, fontFamily: 'Poppins'), ), ], ), ); if (isLocal) { final file = File(pathOrUrl); return file.existsSync() ? Image.file(file, fit: BoxFit.contain) : error; } else { return Image.network( pathOrUrl, fit: BoxFit.contain, errorBuilder: (_, __, ___) => error, ); } } }