809 lines
34 KiB
Dart
809 lines
34 KiB
Dart
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<AdminDashboard> createState() => _AdminDashboardState();
|
|
}
|
|
|
|
class _AdminDashboardState extends State<AdminDashboard> {
|
|
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<void> _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<String, int> _calculateRevenue() {
|
|
final revenue = <String, int>{};
|
|
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]}.');
|
|
}
|
|
}
|