600 lines
19 KiB
Dart
600 lines
19 KiB
Dart
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/firebase_service.dart';
|
|
import '../recommendation/recommendation_screen.dart';
|
|
import '../../services/ocr_service.dart';
|
|
import '../../services/dialog_service.dart';
|
|
|
|
class HistoryScreen extends ConsumerStatefulWidget {
|
|
final int initialIndex;
|
|
|
|
const HistoryScreen({super.key, this.initialIndex = 0});
|
|
|
|
@override
|
|
ConsumerState<HistoryScreen> createState() => _HistoryScreenState();
|
|
}
|
|
|
|
class _HistoryScreenState extends ConsumerState<HistoryScreen>
|
|
with SingleTickerProviderStateMixin {
|
|
late TabController _tabController;
|
|
List<RecommendationModel> _allRecommendations = [];
|
|
List<RecommendationModel> _favoriteRecommendations = [];
|
|
bool _isLoading = true;
|
|
String? _error;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_tabController = TabController(
|
|
length: 2,
|
|
vsync: this,
|
|
initialIndex: widget.initialIndex,
|
|
);
|
|
_loadRecommendations();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_tabController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _loadRecommendations() async {
|
|
setState(() {
|
|
_isLoading = true;
|
|
_error = null;
|
|
});
|
|
|
|
try {
|
|
final firebaseService = FirebaseService();
|
|
final currentUser = firebaseService.currentUser;
|
|
|
|
if (currentUser == null) {
|
|
// For now, show empty state instead of error for unauthenticated users
|
|
if (mounted) {
|
|
setState(() {
|
|
_allRecommendations = [];
|
|
_favoriteRecommendations = [];
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Try to get recommendations with simpler query to avoid index issues
|
|
final recommendations = await firebaseService.getUserRecommendations(
|
|
currentUser.uid,
|
|
limit: 20, // Reduce limit to avoid performance issues
|
|
);
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
// Hanya tampilkan riwayat yang sudah dipilih menunya
|
|
_allRecommendations = recommendations.where((r) => r.selectedMenuName != null).toList();
|
|
_favoriteRecommendations = _allRecommendations
|
|
.where((rec) => rec.isFavorite)
|
|
.toList();
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
setState(() {
|
|
// Show more user-friendly error message
|
|
_error =
|
|
'Tidak dapat memuat riwayat. Pastikan Anda sudah login dan memiliki koneksi internet.';
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: AppTheme.warmWhite,
|
|
appBar: AppBar(
|
|
title: const Text(
|
|
'Riwayat Rekomendasi',
|
|
style: TextStyle(
|
|
color: AppTheme.primaryBrown,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
backgroundColor: AppTheme.warmWhite,
|
|
elevation: 0,
|
|
iconTheme: const IconThemeData(color: AppTheme.primaryBrown),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
onPressed: _loadRecommendations,
|
|
),
|
|
PopupMenuButton(
|
|
icon: const Icon(Icons.more_vert, color: AppTheme.primaryBrown),
|
|
itemBuilder: (context) => [
|
|
const PopupMenuItem(
|
|
value: 'clear_all',
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.clear_all, color: Colors.red),
|
|
SizedBox(width: 8),
|
|
Text('Hapus Semua'),
|
|
],
|
|
),
|
|
),
|
|
const PopupMenuItem(
|
|
value: 'export',
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.download),
|
|
SizedBox(width: 8),
|
|
Text('Export Data'),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
onSelected: (value) {
|
|
if (value == 'clear_all') {
|
|
_showClearAllDialog();
|
|
} else if (value == 'export') {
|
|
_exportData();
|
|
}
|
|
},
|
|
),
|
|
],
|
|
bottom: TabBar(
|
|
controller: _tabController,
|
|
labelColor: AppTheme.primaryBrown,
|
|
unselectedLabelColor: AppTheme.grey500,
|
|
indicatorColor: AppTheme.primaryBrown,
|
|
tabs: [
|
|
Tab(text: 'Semua (${_allRecommendations.length})'),
|
|
Tab(text: 'Favorit (${_favoriteRecommendations.length})'),
|
|
],
|
|
),
|
|
),
|
|
body: _isLoading
|
|
? _buildLoadingState()
|
|
: _error != null
|
|
? _buildErrorState()
|
|
: TabBarView(
|
|
controller: _tabController,
|
|
children: [
|
|
_buildRecommendationsList(_allRecommendations),
|
|
_buildRecommendationsList(_favoriteRecommendations),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLoadingState() {
|
|
return const Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
CircularProgressIndicator(),
|
|
SizedBox(height: 16),
|
|
Text('Memuat riwayat...'),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildErrorState() {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.error_outline, size: 64, color: AppTheme.error),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Gagal memuat riwayat',
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
_error ?? 'Terjadi kesalahan',
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.bodyMedium?.copyWith(color: AppTheme.grey600),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 24),
|
|
ElevatedButton(
|
|
onPressed: _loadRecommendations,
|
|
child: const Text('Coba Lagi'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildRecommendationsList(List<RecommendationModel> recommendations) {
|
|
if (recommendations.isEmpty) {
|
|
return _buildEmptyState();
|
|
}
|
|
|
|
final bottomPadding =
|
|
MediaQuery.of(context).padding.bottom +
|
|
kBottomNavigationBarHeight +
|
|
16.0;
|
|
|
|
return RefreshIndicator(
|
|
onRefresh: _loadRecommendations,
|
|
child: ListView.builder(
|
|
padding: EdgeInsets.fromLTRB(
|
|
AppConstants.defaultPadding,
|
|
AppConstants.defaultPadding,
|
|
AppConstants.defaultPadding,
|
|
bottomPadding,
|
|
),
|
|
itemCount: recommendations.length,
|
|
itemBuilder: (context, index) {
|
|
return _buildRecommendationCard(recommendations[index]);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildEmptyState() {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32.0),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.history, size: 64, color: AppTheme.grey400),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Belum ada riwayat',
|
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: AppTheme.grey700,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Mulai ambil foto menu untuk mendapatkan rekomendasi personal dari AI',
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.bodyMedium?.copyWith(color: AppTheme.grey600),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 24),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
// Navigate to camera or home screen
|
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
|
},
|
|
icon: const Icon(Icons.camera_alt),
|
|
label: const Text('Ambil Foto Menu'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildRecommendationCard(RecommendationModel recommendation) {
|
|
final topRecommendations = recommendation.topRecommendations;
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: InkWell(
|
|
onTap: () => _openRecommendation(recommendation),
|
|
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Header
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
recommendation.formattedDate,
|
|
style: Theme.of(context).textTheme.titleMedium
|
|
?.copyWith(fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'${recommendation.recommendations.length} rekomendasi',
|
|
style: Theme.of(context).textTheme.bodySmall
|
|
?.copyWith(color: AppTheme.grey600),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// Confidence Badge
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: _getConfidenceColor(
|
|
recommendation.confidence,
|
|
).withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: _getConfidenceColor(
|
|
recommendation.confidence,
|
|
).withOpacity(0.3),
|
|
),
|
|
),
|
|
child: Text(
|
|
'${(recommendation.confidence * 100).toInt()}%',
|
|
style: TextStyle(
|
|
color: _getConfidenceColor(recommendation.confidence),
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
// Favorite Icon
|
|
Icon(
|
|
recommendation.isFavorite
|
|
? Icons.favorite
|
|
: Icons.favorite_border,
|
|
color: recommendation.isFavorite
|
|
? AppTheme.error
|
|
: AppTheme.grey400,
|
|
size: 20,
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
// Top Recommendations Preview
|
|
if (topRecommendations.isNotEmpty) ...[
|
|
Text(
|
|
'Rekomendasi Teratas:',
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: AppTheme.grey600,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
...topRecommendations
|
|
.take(2)
|
|
.map((rec) => _buildMiniRecommendationItem(rec))
|
|
.toList(),
|
|
if (topRecommendations.length > 2)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 4),
|
|
child: Text(
|
|
'+${topRecommendations.length - 2} lainnya',
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: AppTheme.primaryBrown,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
// Action Buttons
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () => _openRecommendation(recommendation),
|
|
icon: const Icon(Icons.visibility, size: 16),
|
|
label: const Text('Lihat Detail'),
|
|
style: OutlinedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
IconButton(
|
|
onPressed: () => _toggleFavorite(recommendation),
|
|
icon: Icon(
|
|
recommendation.isFavorite
|
|
? Icons.favorite
|
|
: Icons.favorite_border,
|
|
color: recommendation.isFavorite
|
|
? AppTheme.error
|
|
: AppTheme.grey500,
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () => _showDeleteDialog(recommendation),
|
|
icon: const Icon(
|
|
Icons.delete_outline,
|
|
color: AppTheme.error,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMiniRecommendationItem(MenuRecommendation recommendation) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 4),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 4,
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.primaryBrown,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'${recommendation.menuName} - ${recommendation.formattedPrice}',
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.bodySmall?.copyWith(color: AppTheme.grey700),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
// Rating Stars
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: List.generate(recommendation.starRating, (index) {
|
|
return Icon(Icons.star, color: AppTheme.accent, size: 12);
|
|
}),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Color _getConfidenceColor(double confidence) {
|
|
if (confidence >= 0.8) return AppTheme.success;
|
|
if (confidence >= 0.6) return AppTheme.warning;
|
|
return AppTheme.error;
|
|
}
|
|
|
|
Future<void> _openRecommendation(RecommendationModel recommendation) async {
|
|
// Create a mock OCR result for the recommendation screen
|
|
final ocrResult = OCRResult(
|
|
rawText: recommendation.originalMenuText,
|
|
processedText: recommendation.originalMenuText,
|
|
menuItems: recommendation.recommendations
|
|
.map(
|
|
(rec) => MenuItemOCR(
|
|
name: rec.menuName,
|
|
price: rec.price,
|
|
description: rec.description,
|
|
category: rec.category,
|
|
),
|
|
)
|
|
.toList(),
|
|
confidence: recommendation.confidence,
|
|
isSuccess: true,
|
|
);
|
|
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute(
|
|
builder: (context) => RecommendationScreen(
|
|
recommendation: recommendation,
|
|
ocrResult: ocrResult,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _toggleFavorite(RecommendationModel recommendation) async {
|
|
try {
|
|
final newFavoriteStatus = !recommendation.isFavorite;
|
|
await FirebaseService().toggleRecommendationFavorite(
|
|
recommendation.id,
|
|
newFavoriteStatus,
|
|
);
|
|
|
|
setState(() {
|
|
recommendation.copyWith(isFavorite: newFavoriteStatus);
|
|
_favoriteRecommendations = _allRecommendations
|
|
.where((rec) => rec.isFavorite)
|
|
.toList();
|
|
});
|
|
|
|
DialogService().showSuccess(
|
|
newFavoriteStatus ? 'Ditambahkan ke favorit' : 'Dihapus dari favorit',
|
|
);
|
|
} catch (e) {
|
|
DialogService().showError('Gagal mengupdate favorit: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _showDeleteDialog(RecommendationModel recommendation) async {
|
|
final confirm = await DialogService().showConfirmDialog(
|
|
title: 'Hapus Rekomendasi',
|
|
message: 'Apakah Anda yakin ingin menghapus rekomendasi ini?',
|
|
isDanger: true,
|
|
context: context,
|
|
);
|
|
|
|
if (confirm) {
|
|
await _deleteRecommendation(recommendation);
|
|
}
|
|
}
|
|
|
|
Future<void> _deleteRecommendation(RecommendationModel recommendation) async {
|
|
try {
|
|
await FirebaseService().deleteRecommendation(recommendation.id);
|
|
|
|
setState(() {
|
|
_allRecommendations.removeWhere((rec) => rec.id == recommendation.id);
|
|
_favoriteRecommendations.removeWhere(
|
|
(rec) => rec.id == recommendation.id,
|
|
);
|
|
});
|
|
|
|
DialogService().showSuccess('Rekomendasi berhasil dihapus');
|
|
} catch (e) {
|
|
DialogService().showError('Gagal menghapus rekomendasi: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _showClearAllDialog() async {
|
|
final confirm = await DialogService().showConfirmDialog(
|
|
title: 'Hapus Semua Riwayat',
|
|
message:
|
|
'Apakah Anda yakin ingin menghapus semua riwayat rekomendasi? Tindakan ini tidak dapat dibatalkan.',
|
|
isDanger: true,
|
|
context: context,
|
|
);
|
|
|
|
if (confirm) {
|
|
await _clearAllRecommendations();
|
|
}
|
|
}
|
|
|
|
Future<void> _clearAllRecommendations() async {
|
|
try {
|
|
// Delete all recommendations
|
|
for (final recommendation in _allRecommendations) {
|
|
await FirebaseService().deleteRecommendation(recommendation.id);
|
|
}
|
|
|
|
setState(() {
|
|
_allRecommendations.clear();
|
|
_favoriteRecommendations.clear();
|
|
});
|
|
|
|
DialogService().showSuccess('Semua riwayat berhasil dihapus');
|
|
} catch (e) {
|
|
DialogService().showError('Gagal menghapus riwayat: $e');
|
|
}
|
|
}
|
|
|
|
void _exportData() {
|
|
DialogService().showInfo('Fitur export akan segera hadir');
|
|
}
|
|
}
|