initial commit
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import 'package:intl/intl.dart';
|
||||
import '../models/film.dart';
|
||||
import '../models/booking.dart';
|
||||
|
||||
class AppData {
|
||||
static const List<String> cinemas = [
|
||||
'CGV Grand Indonesia',
|
||||
'Cinepolis Plaza Senayan',
|
||||
'XXI Pacific Place',
|
||||
];
|
||||
|
||||
static Map<String, Map<String, Map<String, List<List<String>>>>>
|
||||
generateSchedules() {
|
||||
final schedules = <String, Map<String, Map<String, List<List<String>>>>>{};
|
||||
final now = DateTime.now();
|
||||
|
||||
// Generate schedules for each cinema
|
||||
for (var cinema in cinemas) {
|
||||
schedules[cinema] = {};
|
||||
|
||||
for (int i = 0; i < 7; i++) {
|
||||
final date = now.add(Duration(days: i));
|
||||
final dateStr = DateFormat('yyyy-MM-dd').format(date);
|
||||
|
||||
schedules[cinema]![dateStr] = {
|
||||
"10:00": List.generate(5, (_) => List.filled(8, 'available')),
|
||||
"13:30": List.generate(5, (_) => List.filled(8, 'available')),
|
||||
"16:00": List.generate(5, (_) => List.filled(8, 'available')),
|
||||
"19:30": List.generate(5, (_) => List.filled(8, 'available')),
|
||||
"22:00": List.generate(5, (_) => List.filled(8, 'available')),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return schedules;
|
||||
}
|
||||
|
||||
static List<Film> films = [
|
||||
Film(
|
||||
id: 1,
|
||||
title: "Despicable Me 4",
|
||||
genre: "Dark Comedy, Parody, Computer Animation",
|
||||
rating: 6.2,
|
||||
description:
|
||||
"Despicable Me 4 adalah film komedi animasi Amerika yang diproduksi oleh Illumination dan didistribusikan oleh Universal Pictures. Film ini bertindak sebagai sekuel bagi film Despicable Me 3, film utama keempat, dan film keenam secara keseluruhan di waralaba Despicable Me.",
|
||||
duration: 94,
|
||||
ageRating: "SU",
|
||||
director: "Patrick Delage , Chris Renaud",
|
||||
cast: ["Steve Carell", "Pierre Coffin", "Will Ferrell"],
|
||||
releaseDate: DateTime(2024, 7, 3),
|
||||
endDate: DateTime(2024, 12, 31),
|
||||
schedules: generateSchedules(),
|
||||
price: 50000,
|
||||
imageUrl: "https://image.tmdb.org/t/p/w500/wWba3TaojhK7NdycRhoQpsG0FaH.jpg",
|
||||
),
|
||||
Film(
|
||||
id: 2,
|
||||
title: "Love in Paris",
|
||||
genre: "Romance, Drama",
|
||||
rating: 7.8,
|
||||
description: "Kisah cinta yang indah di kota paling romantis di dunia.",
|
||||
duration: 118,
|
||||
ageRating: "SU",
|
||||
director: "Sofia Laurent",
|
||||
cast: ["Emma Stone", "Timothée Chalamet"],
|
||||
releaseDate: DateTime(2024, 11, 20),
|
||||
endDate: DateTime(2024, 12, 31),
|
||||
schedules: generateSchedules(),
|
||||
price: 45000,
|
||||
imageUrl: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtNMli6kZnZxVJ67trY2Km_T-Pp0bDKEdd1g&s",
|
||||
),
|
||||
Film(
|
||||
id: 3,
|
||||
title: "Ghost Hunter",
|
||||
genre: "Horror, Thriller",
|
||||
rating: 7.2,
|
||||
description:
|
||||
"Pemburu hantu menghadapi teror supernatural yang mengerikan.",
|
||||
duration: 105,
|
||||
ageRating: "D 17+",
|
||||
director: "James Wan Jr.",
|
||||
cast: ["Patrick Wilson", "Vera Farmiga"],
|
||||
releaseDate: DateTime(2024, 11, 18),
|
||||
endDate: DateTime(2024, 12, 31),
|
||||
schedules: generateSchedules(),
|
||||
price: 50000,
|
||||
imageUrl: "https://m.media-amazon.com/images/M/MV5BMzRmODc3MzQtYzA0Yi00OTNkLWFkM2UtYjcyMDQ3ZDU1ZWYyXkEyXkFqcGc@._V1_.jpg",
|
||||
),
|
||||
Film(
|
||||
id: 4,
|
||||
title: "Comic 8",
|
||||
genre: "Comedy",
|
||||
rating: 8.0,
|
||||
description:
|
||||
"Komedi segar yang akan membuat Anda tertawa sepanjang film.",
|
||||
duration: 105,
|
||||
ageRating: "R 13+",
|
||||
director: "Frederica",
|
||||
cast: ["Mongol Stres", "Mudy Taylor", "Arie Kriting"],
|
||||
releaseDate: DateTime(2014, 1, 29),
|
||||
endDate: DateTime(2024, 12, 31),
|
||||
schedules: generateSchedules(),
|
||||
price: 40000,
|
||||
imageUrl: "https://upload.wikimedia.org/wikipedia/id/a/ac/Comic_8.jpg",
|
||||
),
|
||||
];
|
||||
|
||||
static List<Booking> bookings = [];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'pages/login_page.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const CineSimApp());
|
||||
}
|
||||
|
||||
class CineSimApp extends StatelessWidget {
|
||||
const CineSimApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'CineSim',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData.dark().copyWith(
|
||||
scaffoldBackgroundColor: const Color(0xFF111827),
|
||||
primaryColor: const Color(0xFFFBBF24),
|
||||
),
|
||||
home: const LoginPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
class Booking {
|
||||
final String code;
|
||||
final String film;
|
||||
final String date;
|
||||
final String schedule;
|
||||
final List<String> seats;
|
||||
final int price;
|
||||
final String user;
|
||||
final String cinema;
|
||||
final DateTime bookingTime;
|
||||
final String paymentMethod;
|
||||
|
||||
Booking({
|
||||
required this.code,
|
||||
required this.film,
|
||||
required this.date,
|
||||
required this.schedule,
|
||||
required this.seats,
|
||||
required this.price,
|
||||
required this.user,
|
||||
required this.cinema,
|
||||
required this.bookingTime,
|
||||
required this.paymentMethod,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
class Film {
|
||||
final int id;
|
||||
final String title;
|
||||
final String genre;
|
||||
final double rating;
|
||||
final String description;
|
||||
final int duration;
|
||||
final String ageRating;
|
||||
final String director;
|
||||
final List<String> cast;
|
||||
final DateTime releaseDate;
|
||||
final DateTime endDate;
|
||||
final Map<String, Map<String, Map<String, List<List<String>>>>>
|
||||
schedules; // cinema -> date -> time -> seats
|
||||
final int price;
|
||||
final String? imageUrl;
|
||||
|
||||
Film({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.genre,
|
||||
required this.rating,
|
||||
required this.description,
|
||||
required this.duration,
|
||||
required this.ageRating,
|
||||
required this.director,
|
||||
required this.cast,
|
||||
required this.releaseDate,
|
||||
required this.endDate,
|
||||
required this.schedules,
|
||||
required this.price,
|
||||
this.imageUrl,
|
||||
});
|
||||
|
||||
Film copyWith({
|
||||
int? id,
|
||||
String? title,
|
||||
String? genre,
|
||||
double? rating,
|
||||
String? description,
|
||||
int? duration,
|
||||
String? ageRating,
|
||||
String? director,
|
||||
List<String>? cast,
|
||||
DateTime? releaseDate,
|
||||
DateTime? endDate,
|
||||
Map<String, Map<String, Map<String, List<List<String>>>>>? schedules,
|
||||
int? price,
|
||||
String? imageUrl,
|
||||
}) {
|
||||
return Film(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
genre: genre ?? this.genre,
|
||||
rating: rating ?? this.rating,
|
||||
description: description ?? this.description,
|
||||
duration: duration ?? this.duration,
|
||||
ageRating: ageRating ?? this.ageRating,
|
||||
director: director ?? this.director,
|
||||
cast: cast ?? this.cast,
|
||||
releaseDate: releaseDate ?? this.releaseDate,
|
||||
endDate: endDate ?? this.endDate,
|
||||
schedules: schedules ?? this.schedules,
|
||||
price: price ?? this.price,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,808 @@
|
||||
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]}.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'dart:convert';
|
||||
import '../models/film.dart';
|
||||
import '../widgets/schedule_selection_widget.dart';
|
||||
|
||||
class DetailPage extends StatelessWidget {
|
||||
final Film film;
|
||||
final String username;
|
||||
final bool isGuest;
|
||||
|
||||
const DetailPage({
|
||||
super.key,
|
||||
required this.film,
|
||||
required this.username,
|
||||
required this.isGuest,
|
||||
});
|
||||
|
||||
Widget _buildFilmImage(String? imageUrl,
|
||||
{required double width, required double height}) {
|
||||
if (imageUrl == null || imageUrl.isEmpty) {
|
||||
return Center(
|
||||
child: Icon(Icons.movie, size: 100, color: Colors.grey),
|
||||
);
|
||||
}
|
||||
|
||||
if (imageUrl.startsWith('data:image')) {
|
||||
// Base64 image
|
||||
try {
|
||||
final base64String = imageUrl.split(',')[1];
|
||||
final bytes = base64Decode(base64String);
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Image.memory(
|
||||
bytes,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Center(
|
||||
child: Icon(Icons.movie, size: 100, color: Colors.grey),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
return Center(
|
||||
child: Icon(Icons.movie, size: 100, color: Colors.grey),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Network image (URL)
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Image.network(
|
||||
imageUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Center(
|
||||
child: Icon(Icons.movie, size: 100, color: Colors.grey),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isComingSoon = film.releaseDate.isAfter(DateTime.now());
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.black,
|
||||
title: const Text('Detail Film'),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.person, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(username),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 300,
|
||||
height: 450,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[800],
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildFilmImage(film.imageUrl, width: 300, height: 450),
|
||||
Positioned(
|
||||
top: 12,
|
||||
left: 12,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black87,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
film.ageRating,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 32),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
film.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 36, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.star,
|
||||
size: 28, color: Color(0xFFFBBF24)),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
film.rating.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFFBBF24),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
Text(film.genre,
|
||||
style: const TextStyle(color: Colors.grey)),
|
||||
const SizedBox(width: 24),
|
||||
const Icon(Icons.access_time,
|
||||
size: 20, color: Colors.grey),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${film.duration} menit',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildInfoRow('Sutradara', film.director),
|
||||
const SizedBox(height: 8),
|
||||
_buildInfoRow('Pemain', film.cast.join(', ')),
|
||||
const SizedBox(height: 8),
|
||||
_buildInfoRow(
|
||||
'Tanggal Rilis',
|
||||
DateFormat('dd MMMM yyyy').format(film.releaseDate),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'Sinopsis',
|
||||
style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
film.description,
|
||||
style: const TextStyle(
|
||||
fontSize: 16, color: Colors.grey, height: 1.5),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
if (isComingSoon)
|
||||
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(
|
||||
children: [
|
||||
const Icon(Icons.schedule,
|
||||
size: 48, color: Color(0xFFFBBF24)),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Film ini akan segera tayang',
|
||||
style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Mulai ${DateFormat('dd MMMM yyyy').format(film.releaseDate)}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFBBF24), fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
ScheduleSelectionWidget(
|
||||
film: film,
|
||||
username: username,
|
||||
isGuest: isGuest,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
const Text(': ', style: TextStyle(color: Colors.grey)),
|
||||
Expanded(
|
||||
child:
|
||||
Text(value, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'dart:convert';
|
||||
import '../data/app_data.dart';
|
||||
import '../models/film.dart';
|
||||
import 'login_page.dart';
|
||||
import 'detail_page.dart';
|
||||
import 'my_bookings_page.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
final String username;
|
||||
final bool isGuest;
|
||||
|
||||
const HomePage({super.key, required this.username, required this.isGuest});
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
int _selectedIndex = 0;
|
||||
|
||||
Widget _buildFilmImage(String? imageUrl,
|
||||
{required double width, required double height}) {
|
||||
if (imageUrl == null || imageUrl.isEmpty) {
|
||||
return Center(
|
||||
child: Icon(Icons.movie, size: 80, color: Colors.grey),
|
||||
);
|
||||
}
|
||||
|
||||
if (imageUrl.startsWith('data:image')) {
|
||||
// Base64 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 Center(
|
||||
child: Icon(Icons.movie, size: 80, color: Colors.grey),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
return Center(
|
||||
child: Icon(Icons.movie, size: 80, color: Colors.grey),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Network image (URL)
|
||||
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 Center(
|
||||
child: Icon(Icons.movie, size: 80, color: Colors.grey),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.black,
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.movie, color: Color(0xFFFBBF24)),
|
||||
SizedBox(width: 8),
|
||||
Text('CineSim'),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
if (!widget.isGuest)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.confirmation_number_outlined),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MyBookingsPage(username: widget.username),
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: 'Tiket Saya',
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.person, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(widget.username),
|
||||
const SizedBox(width: 16),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const LoginPage()),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _selectedIndex == 0 ? _buildNowShowingTab() : _buildComingSoonTab(),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
currentIndex: _selectedIndex,
|
||||
onTap: (index) => setState(() => _selectedIndex = index),
|
||||
backgroundColor: const Color(0xFF1F2937),
|
||||
selectedItemColor: const Color(0xFFFBBF24),
|
||||
unselectedItemColor: Colors.grey,
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.play_circle_outline),
|
||||
label: 'Sedang Tayang',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.upcoming),
|
||||
label: 'Coming Soon',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNowShowingTab() {
|
||||
final nowShowingFilms = AppData.films
|
||||
.where((f) => f.releaseDate.isBefore(DateTime.now()))
|
||||
.toList();
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Film Sedang Tayang',
|
||||
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
widget.isGuest
|
||||
? 'Login untuk memesan tiket'
|
||||
: 'Pilih film favorit Anda',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildFilmGrid(nowShowingFilms),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildComingSoonTab() {
|
||||
final comingSoonFilms = AppData.films
|
||||
.where((f) => f.releaseDate.isAfter(DateTime.now()))
|
||||
.toList();
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Coming Soon',
|
||||
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Film yang akan segera tayang',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildFilmGrid(comingSoonFilms),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilmGrid(List<Film> films) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
int crossAxisCount = 4;
|
||||
if (constraints.maxWidth < 1200) crossAxisCount = 3;
|
||||
if (constraints.maxWidth < 900) crossAxisCount = 2;
|
||||
if (constraints.maxWidth < 600) crossAxisCount = 1;
|
||||
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
childAspectRatio: 0.65,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
),
|
||||
itemCount: films.length,
|
||||
itemBuilder: (context, index) {
|
||||
final film = films[index];
|
||||
final isComingSoon = film.releaseDate.isAfter(DateTime.now());
|
||||
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => DetailPage(
|
||||
film: film,
|
||||
username: widget.username,
|
||||
isGuest: widget.isGuest,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: 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: 300,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[800],
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: _buildFilmImage(
|
||||
film.imageUrl,
|
||||
width: double.infinity,
|
||||
height: 300,
|
||||
),
|
||||
),
|
||||
if (isComingSoon)
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFBBF24),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'COMING SOON',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 8,
|
||||
left: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black87,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
film.ageRating,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
film.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.bold),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
film.genre,
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.star,
|
||||
size: 16, color: Color(0xFFFBBF24)),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
film.rating.toString(),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFBBF24),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Icon(Icons.access_time,
|
||||
size: 14, color: Colors.grey),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${film.duration} min',
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isComingSoon) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tayang: ${DateFormat('dd MMM yyyy').format(film.releaseDate)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFFFBBF24),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'home_page.dart';
|
||||
import 'admin_dashboard.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final _usernameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
String _role = 'user';
|
||||
|
||||
void _login() {
|
||||
final username = _usernameController.text.trim();
|
||||
final password = _passwordController.text.trim();
|
||||
|
||||
if (_role == 'admin' && username == 'admin' && password == 'admin123') {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const AdminDashboard()),
|
||||
);
|
||||
} else if (_role == 'user' && username.isNotEmpty && password.isNotEmpty) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => HomePage(username: username, isGuest: false),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Username atau password salah!')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _guestMode() {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const HomePage(username: 'Tamu', isGuest: true),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1F2937),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF374151)),
|
||||
),
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.movie, size: 64, color: Color(0xFFFBBF24)),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'CineSim',
|
||||
style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Simulasi Pemesanan Tiket Bioskop',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
DropdownButtonFormField<String>(
|
||||
value: _role,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Role',
|
||||
filled: true,
|
||||
fillColor: const Color(0xFF374151),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'user', child: Text('User')),
|
||||
DropdownMenuItem(value: 'admin', child: Text('Admin')),
|
||||
],
|
||||
onChanged: (value) => setState(() => _role = value!),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _usernameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Username',
|
||||
hintText: _role == 'admin' ? 'admin' : 'Masukkan username',
|
||||
filled: true,
|
||||
fillColor: const Color(0xFF374151),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
hintText:
|
||||
_role == 'admin' ? 'admin123' : 'Masukkan password',
|
||||
filled: true,
|
||||
fillColor: const Color(0xFF374151),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _login,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFBBF24),
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
child: const Text(
|
||||
'Login',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton(
|
||||
onPressed: _guestMode,
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
side: const BorderSide(color: Color(0xFF374151)),
|
||||
),
|
||||
child: const Text('Lanjutkan sebagai Tamu'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Admin: admin / admin123',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../data/app_data.dart';
|
||||
import 'ticket_page.dart';
|
||||
|
||||
class MyBookingsPage extends StatelessWidget {
|
||||
final String username;
|
||||
|
||||
const MyBookingsPage({super.key, required this.username});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userBookings = AppData.bookings
|
||||
.where((b) => b.user == username)
|
||||
.toList()
|
||||
.reversed
|
||||
.toList();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.black,
|
||||
title: const Text('Tiket Saya'),
|
||||
),
|
||||
body: userBookings.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.confirmation_number_outlined,
|
||||
size: 100,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'Belum ada tiket',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Pesan tiket sekarang!',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFBBF24),
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
child: const Text('Lihat Film'),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(24),
|
||||
itemCount: userBookings.length,
|
||||
itemBuilder: (context, index) {
|
||||
final booking = userBookings[index];
|
||||
final bookingDate = DateTime.parse(booking.date);
|
||||
final isPast = bookingDate.isBefore(DateTime.now());
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1F2937),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF374151)),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => TicketPage(booking: booking),
|
||||
),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isPast
|
||||
? Colors.grey.shade800
|
||||
: const Color(
|
||||
0xFFFBBF24,
|
||||
).withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isPast
|
||||
? Colors.grey
|
||||
: const Color(0xFFFBBF24),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
isPast ? 'SELESAI' : 'AKTIF',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isPast
|
||||
? Colors.grey
|
||||
: const Color(0xFFFBBF24),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
booking.code,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFFBBF24),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
booking.film,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.calendar_today,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
DateFormat(
|
||||
'EEE, dd MMM yyyy',
|
||||
).format(bookingDate),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
const Icon(
|
||||
Icons.access_time,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
booking.schedule,
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.location_on,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
booking.cinema,
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.event_seat,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
booking.seats.join(', '),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(color: Color(0xFF374151)),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Total Pembayaran',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
Text(
|
||||
'Rp ${_formatPrice(booking.price)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFFBBF24),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatPrice(int price) {
|
||||
return price.toString().replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(m) => '${m[1]}.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'dart:math';
|
||||
import '../models/film.dart';
|
||||
import '../models/booking.dart';
|
||||
import '../data/app_data.dart';
|
||||
import 'ticket_page.dart';
|
||||
|
||||
class SeatSelectionPage extends StatefulWidget {
|
||||
final Film film;
|
||||
final String date;
|
||||
final String schedule;
|
||||
final String username;
|
||||
final String cinema;
|
||||
|
||||
const SeatSelectionPage({
|
||||
super.key,
|
||||
required this.film,
|
||||
required this.date,
|
||||
required this.schedule,
|
||||
required this.username,
|
||||
required this.cinema,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SeatSelectionPage> createState() => _SeatSelectionPageState();
|
||||
}
|
||||
|
||||
class _SeatSelectionPageState extends State<SeatSelectionPage> {
|
||||
final List<String> _selectedSeats = [];
|
||||
final rows = ['A', 'B', 'C', 'D', 'E'];
|
||||
String _selectedPayment = 'QRIS';
|
||||
|
||||
void _toggleSeat(int row, int col) {
|
||||
final seats =
|
||||
widget.film.schedules[widget.cinema]![widget.date]![widget.schedule]!;
|
||||
final seatId = '$row-$col';
|
||||
if (seats[row][col] == 'booked') return;
|
||||
|
||||
setState(() {
|
||||
if (_selectedSeats.contains(seatId)) {
|
||||
_selectedSeats.remove(seatId);
|
||||
} else {
|
||||
_selectedSeats.add(seatId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _confirmBooking() {
|
||||
if (_selectedSeats.isEmpty) return;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: const Color(0xFF1F2937),
|
||||
title: const Text('Konfirmasi Pemesanan'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildConfirmRow('Film', widget.film.title),
|
||||
_buildConfirmRow(
|
||||
'Tanggal',
|
||||
DateFormat('dd MMM yyyy').format(DateTime.parse(widget.date)),
|
||||
),
|
||||
_buildConfirmRow('Jam', widget.schedule),
|
||||
_buildConfirmRow('Bioskop', widget.cinema),
|
||||
_buildConfirmRow(
|
||||
'Kursi',
|
||||
_selectedSeats.map((s) {
|
||||
final parts = s.split('-');
|
||||
return '${rows[int.parse(parts[0])]}${int.parse(parts[1]) + 1}';
|
||||
}).join(', '),
|
||||
),
|
||||
_buildConfirmRow('Pembayaran', _selectedPayment),
|
||||
const Divider(color: Color(0xFF374151)),
|
||||
_buildConfirmRow(
|
||||
'Total',
|
||||
'Rp ${_formatPrice(widget.film.price * _selectedSeats.length)}',
|
||||
isTotal: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
_processBooking();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFBBF24),
|
||||
foregroundColor: Colors.black,
|
||||
),
|
||||
child: const Text('Bayar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _processBooking() {
|
||||
final bookingCode =
|
||||
'BK${Random().nextInt(1000000).toString().padLeft(6, '0')}';
|
||||
final seatNames = _selectedSeats.map((s) {
|
||||
final parts = s.split('-');
|
||||
return '${rows[int.parse(parts[0])]}${int.parse(parts[1]) + 1}';
|
||||
}).toList();
|
||||
|
||||
final booking = Booking(
|
||||
code: bookingCode,
|
||||
film: widget.film.title,
|
||||
date: widget.date,
|
||||
schedule: widget.schedule,
|
||||
seats: seatNames,
|
||||
price: widget.film.price * _selectedSeats.length,
|
||||
user: widget.username,
|
||||
cinema: widget.cinema,
|
||||
bookingTime: DateTime.now(),
|
||||
paymentMethod: _selectedPayment,
|
||||
);
|
||||
|
||||
final seats =
|
||||
widget.film.schedules[widget.cinema]![widget.date]![widget.schedule]!;
|
||||
for (var seat in _selectedSeats) {
|
||||
final parts = seat.split('-');
|
||||
seats[int.parse(parts[0])][int.parse(parts[1])] = 'booked';
|
||||
}
|
||||
|
||||
AppData.bookings.add(booking);
|
||||
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => TicketPage(booking: booking)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConfirmRow(String label, String value, {bool isTotal = false}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: isTotal ? Colors.white : Colors.grey,
|
||||
fontWeight: isTotal ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isTotal ? const Color(0xFFFBBF24) : Colors.white,
|
||||
fontSize: isTotal ? 18 : 14,
|
||||
),
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final seats =
|
||||
widget.film.schedules[widget.cinema]![widget.date]![widget.schedule]!;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.black,
|
||||
title: const Text('Pilih Kursi'),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.person, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(widget.username),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 900),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1F2937),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF374151)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
widget.film.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 24, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${DateFormat('EEE, dd MMM yyyy').format(DateTime.parse(widget.date))} • ${widget.schedule}',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.cinema,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFBBF24),
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Container(
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Color(0xFFFBBF24),
|
||||
Colors.transparent
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'LAYAR',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
letterSpacing: 4),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
...List.generate(5, (rowIndex) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 32,
|
||||
child: Text(
|
||||
rows[rowIndex],
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
...List.generate(8, (colIndex) {
|
||||
final status = seats[rowIndex][colIndex];
|
||||
final seatId = '$rowIndex-$colIndex';
|
||||
final isSelected =
|
||||
_selectedSeats.contains(seatId);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4),
|
||||
child: InkWell(
|
||||
onTap: () =>
|
||||
_toggleSeat(rowIndex, colIndex),
|
||||
child: Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
color: status == 'booked'
|
||||
? Colors.red.shade700
|
||||
: isSelected
|
||||
? const Color(0xFFFBBF24)
|
||||
: const Color(0xFF374151),
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(
|
||||
top: Radius.circular(10),
|
||||
),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? const Color(0xFFFBBF24)
|
||||
: Colors.transparent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${rows[rowIndex]}${colIndex + 1}',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: status == 'booked'
|
||||
? Colors.white54
|
||||
: isSelected
|
||||
? Colors.black
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 32),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildLegend(const Color(0xFF374151), 'Tersedia'),
|
||||
const SizedBox(width: 24),
|
||||
_buildLegend(const Color(0xFFFBBF24), 'Dipilih'),
|
||||
const SizedBox(width: 24),
|
||||
_buildLegend(Colors.red.shade700, 'Terisi'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
SizedBox(
|
||||
width: 350,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1F2937),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF374151)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Metode Pembayaran',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...['QRIS', 'GoPay', 'OVO', 'Dana', 'Kartu Kredit']
|
||||
.map((method) {
|
||||
return RadioListTile<String>(
|
||||
value: method,
|
||||
groupValue: _selectedPayment,
|
||||
onChanged: (value) {
|
||||
setState(() => _selectedPayment = value!);
|
||||
},
|
||||
title: Text(method),
|
||||
activeColor: const Color(0xFFFBBF24),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF111827),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF374151)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Kursi Dipilih',
|
||||
style: TextStyle(color: Colors.grey)),
|
||||
Text(
|
||||
_selectedSeats.isEmpty
|
||||
? '-'
|
||||
: _selectedSeats.map((s) {
|
||||
final parts = s.split('-');
|
||||
return '${rows[int.parse(parts[0])]}${int.parse(parts[1]) + 1}';
|
||||
}).join(', '),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Harga per Tiket',
|
||||
style: TextStyle(color: Colors.grey)),
|
||||
Text(
|
||||
'Rp ${_formatPrice(widget.film.price)}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(color: Color(0xFF374151), height: 32),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Total Pembayaran',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16),
|
||||
),
|
||||
Text(
|
||||
'Rp ${_formatPrice(widget.film.price * _selectedSeats.length)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFFBBF24),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _selectedSeats.isEmpty
|
||||
? null
|
||||
: _confirmBooking,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFBBF24),
|
||||
foregroundColor: Colors.black,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 16),
|
||||
disabledBackgroundColor: Colors.grey.shade800,
|
||||
),
|
||||
child: const Text(
|
||||
'Lanjutkan',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLegend(Color color, String label) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: color, borderRadius: BorderRadius.circular(4)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _formatPrice(int price) {
|
||||
return price.toString().replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(m) => '${m[1]}.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../models/booking.dart';
|
||||
import 'home_page.dart';
|
||||
|
||||
class TicketPage extends StatelessWidget {
|
||||
final Booking booking;
|
||||
|
||||
const TicketPage({super.key, required this.booking});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bookingDate = DateTime.parse(booking.date);
|
||||
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 500),
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1F2937),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF374151)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
child: const Icon(Icons.check, color: Colors.white, size: 40),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Pemesanan Berhasil!',
|
||||
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'E-Ticket Anda',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF111827),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFF374151)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.confirmation_number,
|
||||
size: 48,
|
||||
color: Color(0xFFFBBF24),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildTicketRow(
|
||||
'Kode Booking',
|
||||
booking.code,
|
||||
isHighlight: true,
|
||||
),
|
||||
const Divider(color: Color(0xFF374151), height: 32),
|
||||
_buildTicketRow('Film', booking.film),
|
||||
const Divider(color: Color(0xFF374151), height: 32),
|
||||
_buildTicketRow('Bioskop', booking.cinema),
|
||||
const Divider(color: Color(0xFF374151), height: 32),
|
||||
_buildTicketRow(
|
||||
'Tanggal',
|
||||
DateFormat('EEE, dd MMM yyyy').format(bookingDate),
|
||||
),
|
||||
const Divider(color: Color(0xFF374151), height: 32),
|
||||
_buildTicketRow('Jam Tayang', booking.schedule),
|
||||
const Divider(color: Color(0xFF374151), height: 32),
|
||||
_buildTicketRow('Kursi', booking.seats.join(', ')),
|
||||
const Divider(color: Color(0xFF374151), height: 32),
|
||||
_buildTicketRow('Pembayaran', booking.paymentMethod),
|
||||
const Divider(color: Color(0xFF374151), height: 32),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Total Pembayaran',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
Text(
|
||||
'Rp ${_formatPrice(booking.price)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFFBBF24),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade900.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.shade700),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: Colors.blue, size: 20),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Tunjukkan e-ticket ini di loket bioskop',
|
||||
style: TextStyle(fontSize: 12, color: Colors.blue),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
HomePage(username: booking.user, isGuest: false),
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFBBF24),
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
child: const Text(
|
||||
'Kembali ke Beranda',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
HomePage(username: booking.user, isGuest: false),
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
side: const BorderSide(color: Color(0xFF374151)),
|
||||
),
|
||||
child: const Text('Pesan Tiket Lagi'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTicketRow(
|
||||
String label,
|
||||
String value, {
|
||||
bool isHighlight = false,
|
||||
}) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: isHighlight ? const Color(0xFFFBBF24) : Colors.white,
|
||||
fontWeight: isHighlight ? FontWeight.bold : FontWeight.w600,
|
||||
fontSize: isHighlight ? 18 : 14,
|
||||
),
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _formatPrice(int price) {
|
||||
return price.toString().replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(m) => '${m[1]}.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../models/film.dart';
|
||||
import '../data/app_data.dart';
|
||||
import '../pages/login_page.dart';
|
||||
import '../pages/seat_selection_page.dart';
|
||||
|
||||
class ScheduleSelectionWidget extends StatefulWidget {
|
||||
final Film film;
|
||||
final String username;
|
||||
final bool isGuest;
|
||||
|
||||
const ScheduleSelectionWidget({
|
||||
super.key,
|
||||
required this.film,
|
||||
required this.username,
|
||||
required this.isGuest,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ScheduleSelectionWidget> createState() =>
|
||||
_ScheduleSelectionWidgetState();
|
||||
}
|
||||
|
||||
class _ScheduleSelectionWidgetState extends State<ScheduleSelectionWidget> {
|
||||
String? _selectedDate;
|
||||
String _selectedCinema = AppData.cinemas[0];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.film.schedules[_selectedCinema] != null) {
|
||||
_selectedDate = widget.film.schedules[_selectedCinema]!.keys.first;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.calendar_today, color: Color(0xFFFBBF24)),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Pilih Bioskop, Tanggal & Jadwal',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Cinema Selection
|
||||
const Text(
|
||||
'Bioskop',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: AppData.cinemas.map((cinema) {
|
||||
final isSelected = _selectedCinema == cinema;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedCinema = cinema;
|
||||
if (widget.film.schedules[cinema] != null) {
|
||||
_selectedDate = widget.film.schedules[cinema]!.keys.first;
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? const Color(0xFFFBBF24)
|
||||
: const Color(0xFF374151),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? const Color(0xFFFBBF24)
|
||||
: Colors.transparent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
cinema,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isSelected ? Colors.black : Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Date Selection
|
||||
const Text(
|
||||
'Tanggal',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 80,
|
||||
child: widget.film.schedules[_selectedCinema] != null
|
||||
? ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: widget.film.schedules[_selectedCinema]!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final dateStr = widget
|
||||
.film.schedules[_selectedCinema]!.keys
|
||||
.elementAt(index);
|
||||
final date = DateTime.parse(dateStr);
|
||||
final isSelected = _selectedDate == dateStr;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: InkWell(
|
||||
onTap: () => setState(() => _selectedDate = dateStr),
|
||||
child: Container(
|
||||
width: 80,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? const Color(0xFFFBBF24)
|
||||
: const Color(0xFF374151),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('EEE').format(date),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color:
|
||||
isSelected ? Colors.black : Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
DateFormat('dd').format(date),
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isSelected
|
||||
? Colors.black
|
||||
: Colors.white,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat('MMM').format(date),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color:
|
||||
isSelected ? Colors.black : Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: const Center(child: Text('Tidak ada jadwal tersedia')),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (widget.isGuest)
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'Anda harus login untuk memesan tiket',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const LoginPage()),
|
||||
(route) => false,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFBBF24),
|
||||
foregroundColor: Colors.black,
|
||||
),
|
||||
child: const Text('Login Sekarang'),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
)
|
||||
else if (widget.film.schedules[_selectedCinema] != null &&
|
||||
_selectedDate != null) ...[
|
||||
const Divider(color: Color(0xFF374151)),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Jam Tayang',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: widget
|
||||
.film.schedules[_selectedCinema]![_selectedDate]!.keys
|
||||
.map((time) {
|
||||
final seats = widget
|
||||
.film.schedules[_selectedCinema]![_selectedDate]![time]!;
|
||||
final availableSeats = seats
|
||||
.expand((row) => row)
|
||||
.where((s) => s == 'available')
|
||||
.length;
|
||||
|
||||
return ElevatedButton(
|
||||
onPressed: availableSeats > 0
|
||||
? () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => SeatSelectionPage(
|
||||
film: widget.film,
|
||||
date: _selectedDate!,
|
||||
schedule: time,
|
||||
username: widget.username,
|
||||
cinema: _selectedCinema,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF374151),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24, vertical: 16),
|
||||
minimumSize: const Size(120, 60),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
time,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
availableSeats > 0 ? '$availableSeats kursi' : 'Penuh',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: availableSeats > 0
|
||||
? const Color(0xFFFBBF24)
|
||||
: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Divider(color: Color(0xFF374151)),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Harga Tiket', style: TextStyle(color: Colors.grey)),
|
||||
Text(
|
||||
'Rp ${_formatPrice(widget.film.price)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFFBBF24),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatPrice(int price) {
|
||||
return price.toString().replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(m) => '${m[1]}.',
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user