import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'dart:convert'; import 'dart:typed_data'; import '../data/app_data.dart'; import '../models/film.dart'; import 'login_page.dart'; class AdminDashboard extends StatefulWidget { const AdminDashboard({super.key}); @override State createState() => _AdminDashboardState(); } class _AdminDashboardState extends State { final _titleController = TextEditingController(); final _genreController = TextEditingController(); final _ratingController = TextEditingController(); final _descriptionController = TextEditingController(); final _priceController = TextEditingController(); final _durationController = TextEditingController(); final _directorController = TextEditingController(); final _castController = TextEditingController(); final _ageRatingController = TextEditingController(); final ImagePicker _imagePicker = ImagePicker(); Uint8List? _selectedImageBytes; String? _selectedImageBase64; Film? _editingFilm; Future _pickImage() async { try { final XFile? image = await _imagePicker.pickImage( source: ImageSource.gallery, maxWidth: 1200, maxHeight: 1800, imageQuality: 85, ); if (image != null) { final bytes = await image.readAsBytes(); setState(() { _selectedImageBytes = bytes; _selectedImageBase64 = base64Encode(bytes); }); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Gagal memilih gambar: $e')), ); } } } Widget _buildFilmImage(String? imageUrl, {required double width, required double height}) { if (imageUrl == null || imageUrl.isEmpty) { return const Center( child: Icon(Icons.movie, size: 60, color: Colors.grey)); } if (imageUrl.startsWith('data:image')) { try { final base64String = imageUrl.split(',')[1]; final bytes = base64Decode(base64String); return ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: Image.memory( bytes, width: width, height: height, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return const Center( child: Icon(Icons.movie, size: 60, color: Colors.grey)); }, ), ); } catch (e) { return const Center( child: Icon(Icons.movie, size: 60, color: Colors.grey)); } } else { return ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: Image.network( imageUrl, width: width, height: height, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return const Center( child: Icon(Icons.movie, size: 60, color: Colors.grey)); }, ), ); } } void _showAddEditFilmDialog({Film? film}) { _editingFilm = film; _selectedImageBytes = null; _selectedImageBase64 = null; if (film != null) { _titleController.text = film.title; _genreController.text = film.genre; _ratingController.text = film.rating.toString(); _descriptionController.text = film.description; _priceController.text = film.price.toString(); _durationController.text = film.duration.toString(); _directorController.text = film.director; _castController.text = film.cast.join(', '); _ageRatingController.text = film.ageRating; if (film.imageUrl != null && film.imageUrl!.startsWith('data:image')) { try { final base64String = film.imageUrl!.split(',')[1]; _selectedImageBase64 = base64String; _selectedImageBytes = base64Decode(base64String); } catch (e) {} } } else { _titleController.clear(); _genreController.clear(); _ratingController.clear(); _descriptionController.clear(); _priceController.clear(); _durationController.clear(); _directorController.clear(); _castController.clear(); _ageRatingController.text = 'R 13+'; } showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setDialogState) => Dialog( backgroundColor: const Color(0xFF1F2937), child: SingleChildScrollView( child: Container( constraints: const BoxConstraints(maxWidth: 600), padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(film == null ? 'Tambah Film Baru' : 'Edit Film', style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold)), const SizedBox(height: 24), const Text('Poster Film', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)), const SizedBox(height: 8), Container( width: double.infinity, height: 200, decoration: BoxDecoration( color: const Color(0xFF374151), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFF4B5563)), ), child: _selectedImageBytes != null ? Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.memory(_selectedImageBytes!, width: double.infinity, height: 200, fit: BoxFit.cover), ), Positioned( top: 8, right: 8, child: IconButton( icon: const Icon(Icons.close), onPressed: () { setDialogState(() { _selectedImageBytes = null; _selectedImageBase64 = null; }); }, style: IconButton.styleFrom( backgroundColor: Colors.black87, foregroundColor: Colors.white), ), ), ], ) : InkWell( onTap: () async { await _pickImage(); setDialogState(() {}); }, child: const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.add_photo_alternate, size: 48, color: Colors.grey), SizedBox(height: 8), Text('Klik untuk upload poster', style: TextStyle(color: Colors.grey)), SizedBox(height: 4), Text('Format: JPG, PNG (Max 5MB)', style: TextStyle( color: Colors.grey, fontSize: 12)), ], ), ), ), const SizedBox(height: 24), TextField( controller: _titleController, decoration: const InputDecoration( labelText: 'Judul Film *', filled: true, fillColor: Color(0xFF374151), border: OutlineInputBorder())), const SizedBox(height: 16), Row( children: [ Expanded( child: TextField( controller: _genreController, decoration: const InputDecoration( labelText: 'Genre *', hintText: 'Action, Drama', filled: true, fillColor: Color(0xFF374151), border: OutlineInputBorder()))), const SizedBox(width: 16), Expanded( child: TextField( controller: _ageRatingController, decoration: const InputDecoration( labelText: 'Rating Usia *', hintText: 'R 13+, SU, D 17+', filled: true, fillColor: Color(0xFF374151), border: OutlineInputBorder()))), ], ), const SizedBox(height: 16), Row( children: [ Expanded( child: TextField( controller: _ratingController, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: 'Rating (0-10) *', hintText: '8.5', filled: true, fillColor: Color(0xFF374151), border: OutlineInputBorder()))), const SizedBox(width: 16), Expanded( child: TextField( controller: _durationController, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: 'Durasi (menit) *', hintText: '120', filled: true, fillColor: Color(0xFF374151), border: OutlineInputBorder()))), ], ), const SizedBox(height: 16), TextField( controller: _directorController, decoration: const InputDecoration( labelText: 'Sutradara *', filled: true, fillColor: Color(0xFF374151), border: OutlineInputBorder())), const SizedBox(height: 16), TextField( controller: _castController, decoration: const InputDecoration( labelText: 'Pemain *', hintText: 'Actor 1, Actor 2, Actor 3', filled: true, fillColor: Color(0xFF374151), border: OutlineInputBorder())), const SizedBox(height: 16), TextField( controller: _descriptionController, maxLines: 3, decoration: const InputDecoration( labelText: 'Deskripsi *', filled: true, fillColor: Color(0xFF374151), border: OutlineInputBorder())), const SizedBox(height: 16), TextField( controller: _priceController, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: 'Harga (Rp) *', hintText: '50000', filled: true, fillColor: Color(0xFF374151), border: OutlineInputBorder())), const SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Batal')), const SizedBox(width: 12), ElevatedButton( onPressed: () { if (film == null) { _addFilm(); } else { _updateFilm(film); } Navigator.pop(context); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFFBBF24), foregroundColor: Colors.black), child: Text(film == null ? 'Tambah Film' : 'Update Film'), ), ], ), ], ), ), ), ), ), ); } void _addFilm() { if (_titleController.text.isEmpty || _genreController.text.isEmpty || _ratingController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Mohon lengkapi data film yang wajib diisi!'))); return; } final now = DateTime.now(); String? imageDataUrl; if (_selectedImageBase64 != null) { imageDataUrl = 'data:image/jpeg;base64,$_selectedImageBase64'; } final newFilm = Film( id: AppData.films.length + 1, title: _titleController.text, genre: _genreController.text, rating: double.tryParse(_ratingController.text) ?? 7.0, description: _descriptionController.text.isEmpty ? 'Deskripsi film akan segera hadir.' : _descriptionController.text, duration: int.tryParse(_durationController.text) ?? 120, ageRating: _ageRatingController.text.isEmpty ? 'R 13+' : _ageRatingController.text, director: _directorController.text.isEmpty ? 'Unknown Director' : _directorController.text, cast: _castController.text.isEmpty ? ['Cast 1', 'Cast 2'] : _castController.text.split(',').map((e) => e.trim()).toList(), releaseDate: now, endDate: now.add(const Duration(days: 30)), schedules: AppData.generateSchedules(), price: int.tryParse(_priceController.text) ?? 50000, imageUrl: imageDataUrl, ); setState(() => AppData.films.add(newFilm)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Film berhasil ditambahkan!'))); } void _updateFilm(Film oldFilm) { final index = AppData.films.indexWhere((f) => f.id == oldFilm.id); if (index == -1) return; String? imageDataUrl = oldFilm.imageUrl; if (_selectedImageBase64 != null) { imageDataUrl = 'data:image/jpeg;base64,$_selectedImageBase64'; } final updatedFilm = oldFilm.copyWith( title: _titleController.text, genre: _genreController.text, rating: double.tryParse(_ratingController.text) ?? oldFilm.rating, description: _descriptionController.text, duration: int.tryParse(_durationController.text) ?? oldFilm.duration, ageRating: _ageRatingController.text, director: _directorController.text, cast: _castController.text.split(',').map((e) => e.trim()).toList(), price: int.tryParse(_priceController.text) ?? oldFilm.price, imageUrl: imageDataUrl, ); setState(() => AppData.films[index] = updatedFilm); ScaffoldMessenger.of(context) .showSnackBar(const SnackBar(content: Text('Film berhasil diupdate!'))); } void _deleteFilm(Film film) { showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: const Color(0xFF1F2937), title: const Text('Hapus Film'), content: Text('Apakah Anda yakin ingin menghapus "${film.title}"?'), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Batal')), ElevatedButton( onPressed: () { setState(() => AppData.films.removeWhere((f) => f.id == film.id)); Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Film berhasil dihapus!'))); }, style: ElevatedButton.styleFrom(backgroundColor: Colors.red), child: const Text('Hapus'), ), ], ), ); } Map _calculateRevenue() { final revenue = {}; for (var booking in AppData.bookings) { revenue[booking.film] = (revenue[booking.film] ?? 0) + booking.price; } return revenue; } @override Widget build(BuildContext context) { final revenue = _calculateRevenue(); final totalRevenue = revenue.values.fold(0, (sum, val) => sum + val); return Scaffold( appBar: AppBar( backgroundColor: Colors.black, automaticallyImplyLeading: false, title: const Row(children: [ Icon(Icons.movie, color: Color(0xFFFBBF24)), SizedBox(width: 8), Text('CineSim Admin') ]), actions: [ IconButton( icon: const Icon(Icons.logout), onPressed: () => Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const LoginPage()))) ], ), body: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Dashboard Admin', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)), ElevatedButton.icon( onPressed: () => _showAddEditFilmDialog(), icon: const Icon(Icons.add), label: const Text('Tambah Film'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFFBBF24), foregroundColor: Colors.black, padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 16)), ), ], ), const SizedBox(height: 24), Row( children: [ Expanded( child: _buildStatCard(Icons.movie, AppData.films.length.toString(), 'Total Film')), const SizedBox(width: 16), Expanded( child: _buildStatCard(Icons.confirmation_number, AppData.bookings.length.toString(), 'Total Transaksi')), const SizedBox(width: 16), Expanded( child: _buildStatCard( Icons.attach_money, '${(totalRevenue / 1000000).toStringAsFixed(1)}M', 'Total Pendapatan')), ], ), const SizedBox(height: 32), Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: const Color(0xFF1F2937), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFF374151))), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Laporan Pendapatan per Film', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 24), if (revenue.isEmpty) const Center( child: Padding( padding: EdgeInsets.all(48), child: Column(children: [ Icon(Icons.attach_money, size: 64, color: Colors.grey), SizedBox(height: 16), Text('Belum ada transaksi', style: TextStyle(color: Colors.grey)) ]))) else Column( children: [ ...revenue.entries.map((entry) { final filmBookings = AppData.bookings .where((b) => b.film == entry.key) .toList(); final ticketCount = filmBookings.fold( 0, (sum, b) => sum + b.seats.length); return Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF111827), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFF374151))), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text(entry.key, style: const TextStyle( fontWeight: FontWeight.w600))), Text('Rp ${_formatPrice(entry.value)}', style: const TextStyle( color: Color(0xFFFBBF24), fontWeight: FontWeight.bold)), ], ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('${filmBookings.length} transaksi', style: const TextStyle( color: Colors.grey, fontSize: 12)), Text('$ticketCount tiket', style: const TextStyle( color: Colors.grey, fontSize: 12)), ], ), ], ), ); }), const SizedBox(height: 24), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFFBBF24).withOpacity(0.1), borderRadius: BorderRadius.circular(8), border: Border.all( color: const Color(0xFFFBBF24), width: 2)), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Total Keseluruhan', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16)), Text('Rp ${_formatPrice(totalRevenue)}', style: const TextStyle( color: Color(0xFFFBBF24), fontWeight: FontWeight.bold, fontSize: 20)), ], ), ), ], ), ], ), ), const SizedBox(height: 32), const Text('Daftar Film', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 16), LayoutBuilder( builder: (context, constraints) { int crossAxisCount = 4; if (constraints.maxWidth < 1200) crossAxisCount = 3; if (constraints.maxWidth < 900) crossAxisCount = 2; return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, childAspectRatio: 0.65, crossAxisSpacing: 16, mainAxisSpacing: 16), itemCount: AppData.films.length, itemBuilder: (context, index) { final film = AppData.films[index]; int totalSeats = 0; int bookedSeats = 0; film.schedules.forEach((cinema, dates) { dates.forEach((date, times) { times.forEach((time, seats) { totalSeats += seats.expand((row) => row).length; bookedSeats += seats .expand((row) => row) .where((s) => s == 'booked') .length; }); }); }); final occupancy = totalSeats > 0 ? ((bookedSeats / totalSeats) * 100).toInt() : 0; return Container( decoration: BoxDecoration( color: const Color(0xFF1F2937), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFF374151))), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Stack( children: [ Container( height: 250, decoration: BoxDecoration( color: Colors.grey[800], borderRadius: const BorderRadius.vertical( top: Radius.circular(12))), child: _buildFilmImage(film.imageUrl, width: double.infinity, height: 250), ), Positioned( top: 8, right: 8, child: Row( children: [ IconButton( icon: const Icon(Icons.edit, size: 20), onPressed: () => _showAddEditFilmDialog(film: film), style: IconButton.styleFrom( backgroundColor: const Color(0xFFFBBF24), foregroundColor: Colors.black)), const SizedBox(width: 4), IconButton( icon: const Icon(Icons.delete, size: 20), onPressed: () => _deleteFilm(film), style: IconButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white)), ], ), ), ], ), Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(film.title, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14), maxLines: 1, overflow: TextOverflow.ellipsis), const SizedBox(height: 4), Text(film.genre, style: const TextStyle( color: Colors.grey, fontSize: 11)), const SizedBox(height: 8), Row(children: [ const Icon(Icons.star, size: 14, color: Color(0xFFFBBF24)), const SizedBox(width: 4), Text(film.rating.toString(), style: const TextStyle( color: Color(0xFFFBBF24), fontWeight: FontWeight.bold, fontSize: 12)) ]), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: const Color(0xFF111827), borderRadius: BorderRadius.circular(8), border: Border.all( color: const Color(0xFF374151))), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Okupansi', style: TextStyle( color: Colors.grey, fontSize: 10)), Text('$occupancy%', style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 10)) ]), const SizedBox(height: 4), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: occupancy / 100, backgroundColor: const Color(0xFF374151), valueColor: const AlwaysStoppedAnimation< Color>(Color(0xFFFBBF24)), minHeight: 6)), ], ), ), ], ), ), ], ), ); }, ); }, ), ], ), ), ); } Widget _buildStatCard(IconData icon, String value, String label) { return Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: const Color(0xFF1F2937), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFF374151))), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Icon(icon, color: const Color(0xFFFBBF24), size: 32), Text(value, style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold)) ]), const SizedBox(height: 8), Text(label, style: const TextStyle(color: Colors.grey)), ], ), ); } String _formatPrice(int price) { return price.toString().replaceAllMapped( RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (m) => '${m[1]}.'); } }