Feat: add logic fetch data tickets
This commit is contained in:
parent
381f4f6a8f
commit
3d7f7cc35e
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
|||
#Sat Mar 08 16:02:32 WIB 2025
|
||||
#Mon Mar 10 22:32:22 WIB 2025
|
||||
base.0=D\:\\Flutter\\Flutter Project\\e_porter\\build\\app\\intermediates\\dex\\debug\\mergeExtDexDebug\\classes.dex
|
||||
base.1=D\:\\Flutter\\Flutter Project\\e_porter\\build\\app\\intermediates\\dex\\debug\\mergeLibDexDebug\\0\\classes.dex
|
||||
base.2=D\:\\Flutter\\Flutter Project\\e_porter\\build\\app\\intermediates\\dex\\debug\\mergeProjectDexDebug\\0\\classes.dex
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,62 @@
|
|||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
import '../../domain/models/ticket_model.dart';
|
||||
import '../../domain/repositories/ticket_repository.dart';
|
||||
|
||||
class TicketRepositoryImpl implements TicketRepository {
|
||||
final FirebaseFirestore firestore;
|
||||
|
||||
TicketRepositoryImpl({required this.firestore});
|
||||
|
||||
@override
|
||||
Future<List<TicketModel>> getTickets({
|
||||
required String from,
|
||||
required String to,
|
||||
required DateTime leavingDate,
|
||||
}) async {
|
||||
final collection = firestore.collection('tickets');
|
||||
|
||||
// Normalisasi tanggal: ambil awal hari dan akhir hari untuk tanggal yang dicari
|
||||
final startOfDay = DateTime(leavingDate.year, leavingDate.month, leavingDate.day);
|
||||
final endOfDay = startOfDay.add(Duration(days: 1));
|
||||
|
||||
print(
|
||||
"Fetching tickets with parameters: from = $from, to = $to, leavingDate between = ${Timestamp.fromDate(startOfDay)} and ${Timestamp.fromDate(endOfDay)}");
|
||||
|
||||
final snapshot = await collection
|
||||
.where('from', isEqualTo: from)
|
||||
.where('to', isEqualTo: to)
|
||||
.where('leavingDate', isGreaterThanOrEqualTo: Timestamp.fromDate(startOfDay))
|
||||
.where('leavingDate', isLessThan: Timestamp.fromDate(endOfDay))
|
||||
.get();
|
||||
|
||||
print("Number of tickets found: ${snapshot.docs.length}");
|
||||
snapshot.docs.forEach((doc) {
|
||||
print("Doc ID: ${doc.id} => ${doc.data()}");
|
||||
});
|
||||
|
||||
return snapshot.docs.map((doc) => TicketModel.fromDocument(doc)).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<FlightModel>> getFlights({
|
||||
required String ticketId,
|
||||
required String flightClass,
|
||||
//required String airlineName,
|
||||
}) async {
|
||||
final subCollection = firestore.collection('tickets').doc(ticketId).collection('flights');
|
||||
|
||||
Query query = subCollection;
|
||||
if (flightClass.isNotEmpty) {
|
||||
query = query.where('seatClass', isEqualTo: flightClass);
|
||||
}
|
||||
|
||||
final snapshot = await query.get();
|
||||
print("Number of flights found for ticket $ticketId with seatClass '$flightClass': ${snapshot.docs.length}");
|
||||
snapshot.docs.forEach((doc) {
|
||||
print("Flight Doc ID: ${doc.id} => ${doc.data()}");
|
||||
});
|
||||
|
||||
return snapshot.docs.map((doc) => FlightModel.fromDocument(doc)).toList();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../data/repositories/ticket_repository_impl.dart';
|
||||
import '../../presentation/controllers/ticket_controller.dart';
|
||||
import '../repositories/ticket_repository.dart';
|
||||
import '../usecases/ticket_usecase.dart';
|
||||
|
||||
class TicketBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// 1. Daftarkan Repository
|
||||
Get.lazyPut<TicketRepository>(
|
||||
() => TicketRepositoryImpl(firestore: FirebaseFirestore.instance),
|
||||
);
|
||||
|
||||
// 2. Daftarkan Usecase
|
||||
Get.lazyPut<SearchFlightUseCase>(
|
||||
() => SearchFlightUseCase(Get.find()),
|
||||
);
|
||||
|
||||
// 3. Daftarkan Controller
|
||||
Get.lazyPut<TicketController>(
|
||||
() => TicketController(Get.find()),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,15 +1,15 @@
|
|||
class Airport {
|
||||
final String city;
|
||||
final String code;
|
||||
final String name;
|
||||
final String? city;
|
||||
final String? code;
|
||||
final String? name;
|
||||
|
||||
Airport({required this.city, required this.code, required this.name});
|
||||
|
||||
factory Airport.fromMap(Map<String, dynamic> map) {
|
||||
return Airport(
|
||||
city: map['city'],
|
||||
code: map['kode'],
|
||||
name: map['name'],
|
||||
city: map['city'] as String?,
|
||||
code: map['kode'] as String?,
|
||||
name: map['name'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
class TicketModel {
|
||||
final String id; // ID dokumen Firestore
|
||||
final String from; // Contoh: "YIA - Yogyakarta"
|
||||
final String to; // Contoh: "LOP - Lombok"
|
||||
final DateTime leavingDate; // Tanggal berangkat
|
||||
|
||||
TicketModel({
|
||||
required this.id,
|
||||
required this.from,
|
||||
required this.to,
|
||||
required this.leavingDate,
|
||||
});
|
||||
|
||||
factory TicketModel.fromDocument(DocumentSnapshot doc) {
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
|
||||
print("TicketModel.fromDocument - Doc ID: ${doc.id}, data: $data");
|
||||
return TicketModel(
|
||||
id: doc.id,
|
||||
from: data['from'] ?? '',
|
||||
to: data['to'] ?? '',
|
||||
leavingDate: (data['leavingDate'] as Timestamp).toDate(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'from': from,
|
||||
'to': to,
|
||||
'leavingDate': Timestamp.fromDate(leavingDate),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class FlightModel {
|
||||
final String id;
|
||||
final String flightClass;
|
||||
final String code;
|
||||
final String cityDeparture;
|
||||
final String cityArrival;
|
||||
final String codeDeparture;
|
||||
final String codeArrival;
|
||||
final DateTime departureTime;
|
||||
final DateTime arrivalTime;
|
||||
final int price;
|
||||
final Map<String, SeatInfo> seat;
|
||||
|
||||
FlightModel({
|
||||
required this.id,
|
||||
required this.flightClass,
|
||||
required this.code,
|
||||
required this.cityDeparture,
|
||||
required this.cityArrival,
|
||||
required this.codeDeparture,
|
||||
required this.codeArrival,
|
||||
required this.departureTime,
|
||||
required this.arrivalTime,
|
||||
required this.price,
|
||||
required this.seat,
|
||||
});
|
||||
|
||||
factory FlightModel.fromDocument(DocumentSnapshot doc) {
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
|
||||
// Ambil field seat (Map) dari Firestore
|
||||
final seatData = data['seat'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
// Konversi setiap entry di seatData menjadi SeatInfo
|
||||
final seatMap = seatData.map<String, SeatInfo>((key, value) {
|
||||
return MapEntry(
|
||||
key,
|
||||
SeatInfo.fromMap(value as Map<String, dynamic>),
|
||||
);
|
||||
});
|
||||
|
||||
print("FlightModel.fromDocument - Doc ID: ${doc.id}, data: $data");
|
||||
|
||||
return FlightModel(
|
||||
id: doc.id,
|
||||
flightClass: data['seatClass'] ?? '',
|
||||
code: data['code'] ?? '',
|
||||
cityDeparture: data['cityDeparture'] ?? '',
|
||||
cityArrival: data['cityArrival'] ?? '',
|
||||
codeDeparture: data['codeDeparture'] ?? '',
|
||||
codeArrival: data['codeArrival'] ?? '',
|
||||
departureTime: (data['dateDeparture'] as Timestamp).toDate(),
|
||||
arrivalTime: (data['dateArrival'] as Timestamp).toDate(),
|
||||
price: data['price'] ?? 0,
|
||||
seat: seatMap, // masukkan Map<String, SeatInfo>
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'seatClass': flightClass,
|
||||
'code': code,
|
||||
'cityDeparture': cityDeparture,
|
||||
'cityArrival': cityArrival,
|
||||
'codeDeparture': codeDeparture,
|
||||
'codeArrival': codeArrival,
|
||||
'dateDeparture': Timestamp.fromDate(departureTime),
|
||||
'dateArrival': Timestamp.fromDate(arrivalTime),
|
||||
'price': price,
|
||||
// Konversi Map<String, SeatInfo> jadi Map<String, Map<String,dynamic>>
|
||||
'seat': seat.map((key, value) => MapEntry(key, value.toMap())),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SeatInfo {
|
||||
final List<bool> isTaken;
|
||||
final int totalSeat;
|
||||
|
||||
SeatInfo({
|
||||
required this.isTaken,
|
||||
required this.totalSeat,
|
||||
});
|
||||
|
||||
factory SeatInfo.fromMap(Map<String, dynamic> map) {
|
||||
final isTakenList = (map['isTaken'] as List<dynamic>? ?? []).map((item) => item as bool).toList();
|
||||
|
||||
return SeatInfo(
|
||||
isTaken: isTakenList,
|
||||
totalSeat: map['totalSeat'] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'isTaken': isTaken,
|
||||
'totalSeat': totalSeat,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TicketFlightModel {
|
||||
final TicketModel ticket;
|
||||
final FlightModel flight;
|
||||
|
||||
TicketFlightModel({
|
||||
required this.ticket,
|
||||
required this.flight,
|
||||
});
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import '../models/ticket_model.dart';
|
||||
|
||||
abstract class TicketRepository {
|
||||
Future<List<TicketModel>> getTickets({
|
||||
required String from,
|
||||
required String to,
|
||||
required DateTime leavingDate,
|
||||
});
|
||||
|
||||
Future<List<FlightModel>> getFlights({
|
||||
required String ticketId,
|
||||
required String flightClass,
|
||||
});
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import '../models/ticket_model.dart';
|
||||
import '../repositories/ticket_repository.dart';
|
||||
|
||||
class SearchFlightUseCase {
|
||||
final TicketRepository repository;
|
||||
|
||||
SearchFlightUseCase(this.repository);
|
||||
|
||||
Future<List<TicketModel>> call({
|
||||
required String from,
|
||||
required String to,
|
||||
required DateTime leavingDate,
|
||||
}) {
|
||||
return repository.getTickets(
|
||||
from: from,
|
||||
to: to,
|
||||
leavingDate: leavingDate,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -33,9 +33,9 @@ class SearchFlightController extends GetxController {
|
|||
} else {
|
||||
filteredAirports.assignAll(airports
|
||||
.where((airport) =>
|
||||
airport.name.toLowerCase().contains(lowerCaseQuery) ||
|
||||
airport.city.toLowerCase().contains(lowerCaseQuery) ||
|
||||
airport.code.toLowerCase().contains(lowerCaseQuery))
|
||||
airport.name!.toLowerCase().contains(lowerCaseQuery) ||
|
||||
airport.city!.toLowerCase().contains(lowerCaseQuery) ||
|
||||
airport.code!.toLowerCase().contains(lowerCaseQuery))
|
||||
.toList());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import 'package:e_porter/domain/repositories/ticket_repository.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../domain/models/ticket_model.dart';
|
||||
import '../../domain/usecases/ticket_usecase.dart';
|
||||
|
||||
class TicketController extends GetxController {
|
||||
final SearchFlightUseCase searchFlightUseCase;
|
||||
final TicketRepository ticketRepository;
|
||||
|
||||
TicketController(this.searchFlightUseCase) : ticketRepository = searchFlightUseCase.repository;
|
||||
|
||||
var tickets = <TicketModel>[].obs;
|
||||
var ticketFlight = <TicketFlightModel>[].obs;
|
||||
var isLoading = false.obs;
|
||||
var errorMessage = ''.obs;
|
||||
|
||||
Future<void> searchTickets({
|
||||
required String from,
|
||||
required String to,
|
||||
required DateTime leavingDate,
|
||||
required String flightClass,
|
||||
}) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
tickets.clear();
|
||||
ticketFlight.clear();
|
||||
|
||||
final ticketList = await searchFlightUseCase(
|
||||
from: from,
|
||||
to: to,
|
||||
leavingDate: leavingDate,
|
||||
);
|
||||
tickets.addAll(ticketList);
|
||||
|
||||
for (final t in ticketList) {
|
||||
final flights = await ticketRepository.getFlights(
|
||||
ticketId: t.id,
|
||||
flightClass: flightClass,
|
||||
);
|
||||
for (final f in flights) {
|
||||
ticketFlight.add(TicketFlightModel(ticket: t, flight: f));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = e.toString();
|
||||
print("searchTickets error: $e");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -50,6 +50,8 @@ class FlightClassRadio extends StatelessWidget {
|
|||
TypographyStyles.body(
|
||||
title,
|
||||
color: GrayColors.gray800,
|
||||
maxlines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
TypographyStyles.caption(
|
||||
|
|
|
@ -9,13 +9,17 @@ class FlightSelector extends StatelessWidget {
|
|||
final String hintText;
|
||||
final Widget svgIconPath;
|
||||
final VoidCallback? onTap;
|
||||
final double? width;
|
||||
final double? widthText;
|
||||
|
||||
const FlightSelector({
|
||||
Key? key,
|
||||
required this.label,
|
||||
required this.hintText,
|
||||
required this.svgIconPath,
|
||||
required this.onTap
|
||||
required this.onTap,
|
||||
this.width,
|
||||
this.widthText,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -24,6 +28,7 @@ class FlightSelector extends StatelessWidget {
|
|||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
|
||||
width: width,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
|
@ -45,11 +50,15 @@ class FlightSelector extends StatelessWidget {
|
|||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
TypographyStyles.body(
|
||||
Container(
|
||||
width: widthText,
|
||||
child: TypographyStyles.body(
|
||||
hintText,
|
||||
color: GrayColors.gray800,
|
||||
fontWeight: FontWeight.w500,
|
||||
maxlines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
|
@ -26,11 +26,13 @@ class BookingTickets extends StatefulWidget {
|
|||
class _BookingTicketsState extends State<BookingTickets> {
|
||||
DateTime selectedDate = DateTime.now();
|
||||
String selectedDateText = 'dd/mm/yyyy';
|
||||
final ValueNotifier<String> selectedClass = ValueNotifier<String>('Economy');
|
||||
final ValueNotifier<String> selectedClass = ValueNotifier<String>('');
|
||||
|
||||
Airport? selectedAirportFrom;
|
||||
Airport? selectedAirportTo;
|
||||
|
||||
int selectedPassengerCount = 1;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -64,6 +66,7 @@ class _BookingTicketsState extends State<BookingTickets> {
|
|||
setState(() {
|
||||
selectedAirportFrom = result;
|
||||
});
|
||||
print('Daparture: ${selectedAirportFrom!.code} - ${selectedAirportFrom!.city}');
|
||||
}
|
||||
},
|
||||
),
|
||||
|
@ -122,9 +125,14 @@ class _BookingTicketsState extends State<BookingTickets> {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
FlightSelector(
|
||||
ValueListenableBuilder<String>(
|
||||
valueListenable: selectedClass,
|
||||
builder: (context, value, child) {
|
||||
return FlightSelector(
|
||||
// width: MediaQuery.of(context).size.height * 0.21,
|
||||
widthText: MediaQuery.of(context).size.height * 0.12,
|
||||
label: 'Kelas penerbangan',
|
||||
hintText: 'Pilih Kelas',
|
||||
hintText: value.isEmpty ? 'Pilih Kelas' : value,
|
||||
svgIconPath: CustomeIcons.FlightSeatOutline(),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
|
@ -150,10 +158,14 @@ class _BookingTicketsState extends State<BookingTickets> {
|
|||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
FlightSelector(
|
||||
// width: MediaQuery.of(context).size.height * 0.17,
|
||||
widthText: 80.w,
|
||||
label: 'Penumpang',
|
||||
hintText: '1 Dewasa',
|
||||
hintText: '${selectedPassengerCount} Dewasa',
|
||||
svgIconPath: CustomeIcons.PassengerOutline(),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
|
@ -189,12 +201,25 @@ class _BookingTicketsState extends State<BookingTickets> {
|
|||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
ButtonFill(
|
||||
ZoomTapAnimation(
|
||||
child: ButtonFill(
|
||||
text: 'Cari Tiket',
|
||||
textColor: Colors.white,
|
||||
onTap: () {
|
||||
Get.toNamed(Routes.SEARCHTICKETS);
|
||||
if (selectedAirportFrom != null && selectedAirportTo != null) {
|
||||
final searchParams = {
|
||||
"from": '${selectedAirportFrom!.city}',
|
||||
"to": '${selectedAirportTo!.city}',
|
||||
"leavingDate": selectedDate,
|
||||
"flightClass": selectedClass.value,
|
||||
"passengerCount": selectedPassengerCount,
|
||||
};
|
||||
Get.toNamed(Routes.SEARCHTICKETS, arguments: searchParams);
|
||||
} else {
|
||||
Get.snackbar("Error", "Silakan pilih bandara keberangkatan dan tujuan");
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
@ -303,6 +328,9 @@ class _BookingTicketsState extends State<BookingTickets> {
|
|||
}
|
||||
|
||||
Widget _buildFlightAddPassenger() {
|
||||
int tempPassengerCount = selectedPassengerCount;
|
||||
return StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
@ -322,13 +350,17 @@ class _BookingTicketsState extends State<BookingTickets> {
|
|||
Container(
|
||||
height: 100.h,
|
||||
child: CupertinoPicker(
|
||||
scrollController: FixedExtentScrollController(initialItem: 0), // Start at 1
|
||||
itemExtent: 45.0, // Set the height of each item
|
||||
scrollController: FixedExtentScrollController(initialItem: tempPassengerCount - 1),
|
||||
itemExtent: 45.0,
|
||||
selectionOverlay: CircleAvatar(
|
||||
radius: 18.r,
|
||||
backgroundColor: PrimaryColors.primary800.withOpacity(0.7),
|
||||
),
|
||||
onSelectedItemChanged: (index) {},
|
||||
onSelectedItemChanged: (index) {
|
||||
setModalState(() {
|
||||
tempPassengerCount = index + 1;
|
||||
});
|
||||
},
|
||||
children: List<Widget>.generate(10, (index) {
|
||||
return Center(
|
||||
child: TypographyStyles.body('${index + 1}'),
|
||||
|
@ -336,42 +368,6 @@ class _BookingTicketsState extends State<BookingTickets> {
|
|||
}),
|
||||
),
|
||||
),
|
||||
// CircleAvatar(
|
||||
// radius: 18.r,
|
||||
// backgroundColor: PrimaryColors.primary800,
|
||||
// child: Obx(
|
||||
// () => TypographyStyles.body(
|
||||
// '${bookingTiketcController.selectedNumber.value}',
|
||||
// color: Colors.white,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(height: 10),
|
||||
// CupertinoPicker(
|
||||
// itemExtent: 50, // Tinggi tiap item
|
||||
// backgroundColor: Colors.white,
|
||||
// selectionOverlay: Container(
|
||||
// color: Colors.white.withOpacity(0.1),
|
||||
// ),
|
||||
// scrollController: FixedExtentScrollController(
|
||||
// initialItem: bookingTiketcController.selectedNumber.value - 1), // Set default pilihan
|
||||
// onSelectedItemChanged: (int index) {
|
||||
// bookingTiketcController.updateNumber(index + 1);
|
||||
// },
|
||||
// children: List.generate(
|
||||
// 10,
|
||||
// (index) => Center(
|
||||
// child: CircleAvatar(
|
||||
// radius: 18.r,
|
||||
// backgroundColor: GrayColors.gray50,
|
||||
// child: TypographyStyles.body(
|
||||
// '${index + 1}',
|
||||
// color: GrayColors.gray300,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
SizedBox(height: 20.h),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 16.h),
|
||||
|
@ -379,12 +375,18 @@ class _BookingTicketsState extends State<BookingTickets> {
|
|||
child: ButtonFill(
|
||||
text: 'Selesai',
|
||||
textColor: Colors.white,
|
||||
onTap: () {},
|
||||
onTap: () {
|
||||
setState(() {
|
||||
selectedPassengerCount = tempPassengerCount;
|
||||
});
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
// SizedBox(height: 16.h)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,9 @@ import 'package:e_porter/presentation/screens/routes/app_rountes.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../controllers/ticket_controller.dart';
|
||||
|
||||
class SearchTicketsScreen extends StatefulWidget {
|
||||
const SearchTicketsScreen({super.key});
|
||||
|
@ -15,15 +18,47 @@ class SearchTicketsScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _SearchTicketsScreenState extends State<SearchTicketsScreen> {
|
||||
late final TicketController ticketController;
|
||||
late final String from;
|
||||
late final String to;
|
||||
late final DateTime leavingDate;
|
||||
late final String flightClass;
|
||||
late final int passengerCount;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Ambil parameter yang dikirim dari BookingTickets
|
||||
final args = Get.arguments as Map<String, dynamic>;
|
||||
from = args['from'];
|
||||
to = args['to'];
|
||||
leavingDate = args['leavingDate'];
|
||||
flightClass = args['flightClass'] ?? '';
|
||||
passengerCount = args['passengerCount'] ?? 1;
|
||||
|
||||
ticketController = Get.find<TicketController>();
|
||||
|
||||
ticketController.searchTickets(
|
||||
from: from,
|
||||
to: to,
|
||||
leavingDate: leavingDate,
|
||||
flightClass: flightClass,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final formattedDate = DateFormat('EEE, d MMM yyyy').format(leavingDate);
|
||||
final currencyFormatter = NumberFormat.decimalPattern('id_ID');
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: GrayColors.gray50,
|
||||
appBar: CustomeAppbarComponent(
|
||||
valueDari: 'Yogyakarta',
|
||||
valueKe: 'Lombok',
|
||||
date: 'Sen, 27 Jn 2025',
|
||||
passenger: '2',
|
||||
valueDari: from,
|
||||
valueKe: to,
|
||||
date: '$formattedDate',
|
||||
passenger: '$passengerCount',
|
||||
onTab: () {
|
||||
Get.back();
|
||||
},
|
||||
|
@ -34,33 +69,69 @@ class _SearchTicketsScreenState extends State<SearchTicketsScreen> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TypographyStyles.h6('Pilih Pesawat', color: GrayColors.gray800, letterSpacing: 0.2),
|
||||
TypographyStyles.h6(
|
||||
'Pilih Pesawat',
|
||||
color: GrayColors.gray800,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
child: Obx(() {
|
||||
if (ticketController.isLoading.value) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
} else if (ticketController.ticketFlight.isEmpty) {
|
||||
return Center(
|
||||
child: TypographyStyles.body(
|
||||
"Tiket tidak ditemukan",
|
||||
color: GrayColors.gray600,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: 5,
|
||||
itemCount: ticketController.ticketFlight.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = ticketController.ticketFlight[index];
|
||||
final ticket = item.ticket;
|
||||
final flight = item.flight;
|
||||
|
||||
final ticketDate = DateFormat("EEE, d MMM").format(ticket.leavingDate);
|
||||
final departureTime = DateFormat.jm().format(flight.departureTime);
|
||||
final arrivalTime = DateFormat.jm().format(flight.arrivalTime);
|
||||
final formattedPrice = 'Rp ${currencyFormatter.format(flight.price)}';
|
||||
|
||||
Duration diff = flight.arrivalTime.difference(flight.departureTime);
|
||||
if (diff.isNegative) {
|
||||
diff = diff + Duration(days: 1);
|
||||
}
|
||||
final hours = diff.inHours;
|
||||
final minutes = diff.inMinutes % 60;
|
||||
final durationFormatted = '${hours}j ${minutes}m';
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 16.h),
|
||||
child: CardTickets(
|
||||
departureCity: 'Yogyakarta (YIA)',
|
||||
date: 'Sen, 27 Jan',
|
||||
arrivalCity: 'Lombok (LOP)',
|
||||
departureCode: 'YIA',
|
||||
arrivalCode: 'LOP',
|
||||
departureTime: '12.20 AM',
|
||||
arrivalTime: '06.00 AM',
|
||||
duration: '5j 40m',
|
||||
seatClass: 'Economy',
|
||||
price: 'Rp 1.200.000',
|
||||
departureCity: '${flight.cityDeparture} (${flight.codeDeparture})',
|
||||
date: ticketDate,
|
||||
arrivalCity: '${flight.cityArrival} (${flight.codeArrival})',
|
||||
departureCode: '${flight.codeDeparture}',
|
||||
arrivalCode: '${flight.codeArrival}',
|
||||
departureTime: departureTime,
|
||||
arrivalTime: arrivalTime,
|
||||
duration: durationFormatted,
|
||||
seatClass: flight.flightClass,
|
||||
price: formattedPrice,
|
||||
onTap: () {
|
||||
Get.toNamed(Routes.TICKETBOOKINGSTEP1);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:e_porter/domain/bindings/auth_binding.dart';
|
||||
import 'package:e_porter/domain/bindings/navigation_binding.dart';
|
||||
import 'package:e_porter/domain/bindings/search_flight_binding.dart';
|
||||
import 'package:e_porter/domain/bindings/ticket_binding.dart';
|
||||
import 'package:e_porter/presentation/screens/auth/pages/forget_password_screen.dart';
|
||||
import 'package:e_porter/presentation/screens/auth/pages/login_screen.dart';
|
||||
import 'package:e_porter/presentation/screens/auth/pages/register_screen.dart';
|
||||
|
@ -81,6 +82,7 @@ class AppRoutes {
|
|||
GetPage(
|
||||
name: Routes.SEARCHTICKETS,
|
||||
page: () => SearchTicketsScreen(),
|
||||
binding: TicketBinding()
|
||||
),
|
||||
GetPage(
|
||||
name: Routes.TICKETBOOKINGSTEP1,
|
||||
|
|
Loading…
Reference in New Issue