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.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.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
|
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 {
|
class Airport {
|
||||||
final String city;
|
final String? city;
|
||||||
final String code;
|
final String? code;
|
||||||
final String name;
|
final String? name;
|
||||||
|
|
||||||
Airport({required this.city, required this.code, required this.name});
|
Airport({required this.city, required this.code, required this.name});
|
||||||
|
|
||||||
factory Airport.fromMap(Map<String, dynamic> map) {
|
factory Airport.fromMap(Map<String, dynamic> map) {
|
||||||
return Airport(
|
return Airport(
|
||||||
city: map['city'],
|
city: map['city'] as String?,
|
||||||
code: map['kode'],
|
code: map['kode'] as String?,
|
||||||
name: map['name'],
|
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 {
|
} else {
|
||||||
filteredAirports.assignAll(airports
|
filteredAirports.assignAll(airports
|
||||||
.where((airport) =>
|
.where((airport) =>
|
||||||
airport.name.toLowerCase().contains(lowerCaseQuery) ||
|
airport.name!.toLowerCase().contains(lowerCaseQuery) ||
|
||||||
airport.city.toLowerCase().contains(lowerCaseQuery) ||
|
airport.city!.toLowerCase().contains(lowerCaseQuery) ||
|
||||||
airport.code.toLowerCase().contains(lowerCaseQuery))
|
airport.code!.toLowerCase().contains(lowerCaseQuery))
|
||||||
.toList());
|
.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(
|
TypographyStyles.body(
|
||||||
title,
|
title,
|
||||||
color: GrayColors.gray800,
|
color: GrayColors.gray800,
|
||||||
|
maxlines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
SizedBox(height: 4.h),
|
SizedBox(height: 4.h),
|
||||||
TypographyStyles.caption(
|
TypographyStyles.caption(
|
||||||
|
|
|
@ -9,13 +9,17 @@ class FlightSelector extends StatelessWidget {
|
||||||
final String hintText;
|
final String hintText;
|
||||||
final Widget svgIconPath;
|
final Widget svgIconPath;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
final double? width;
|
||||||
|
final double? widthText;
|
||||||
|
|
||||||
const FlightSelector({
|
const FlightSelector({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.hintText,
|
required this.hintText,
|
||||||
required this.svgIconPath,
|
required this.svgIconPath,
|
||||||
required this.onTap
|
required this.onTap,
|
||||||
|
this.width,
|
||||||
|
this.widthText,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -24,6 +28,7 @@ class FlightSelector extends StatelessWidget {
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
|
||||||
|
width: width,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(10.r),
|
borderRadius: BorderRadius.circular(10.r),
|
||||||
|
@ -45,11 +50,15 @@ class FlightSelector extends StatelessWidget {
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
),
|
),
|
||||||
SizedBox(height: 4.h),
|
SizedBox(height: 4.h),
|
||||||
TypographyStyles.body(
|
Container(
|
||||||
|
width: widthText,
|
||||||
|
child: TypographyStyles.body(
|
||||||
hintText,
|
hintText,
|
||||||
color: GrayColors.gray800,
|
color: GrayColors.gray800,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
maxlines: 1,
|
maxlines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -26,11 +26,13 @@ class BookingTickets extends StatefulWidget {
|
||||||
class _BookingTicketsState extends State<BookingTickets> {
|
class _BookingTicketsState extends State<BookingTickets> {
|
||||||
DateTime selectedDate = DateTime.now();
|
DateTime selectedDate = DateTime.now();
|
||||||
String selectedDateText = 'dd/mm/yyyy';
|
String selectedDateText = 'dd/mm/yyyy';
|
||||||
final ValueNotifier<String> selectedClass = ValueNotifier<String>('Economy');
|
final ValueNotifier<String> selectedClass = ValueNotifier<String>('');
|
||||||
|
|
||||||
Airport? selectedAirportFrom;
|
Airport? selectedAirportFrom;
|
||||||
Airport? selectedAirportTo;
|
Airport? selectedAirportTo;
|
||||||
|
|
||||||
|
int selectedPassengerCount = 1;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
@ -64,6 +66,7 @@ class _BookingTicketsState extends State<BookingTickets> {
|
||||||
setState(() {
|
setState(() {
|
||||||
selectedAirportFrom = result;
|
selectedAirportFrom = result;
|
||||||
});
|
});
|
||||||
|
print('Daparture: ${selectedAirportFrom!.code} - ${selectedAirportFrom!.city}');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -122,9 +125,14 @@ class _BookingTicketsState extends State<BookingTickets> {
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
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',
|
label: 'Kelas penerbangan',
|
||||||
hintText: 'Pilih Kelas',
|
hintText: value.isEmpty ? 'Pilih Kelas' : value,
|
||||||
svgIconPath: CustomeIcons.FlightSeatOutline(),
|
svgIconPath: CustomeIcons.FlightSeatOutline(),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
|
@ -150,10 +158,14 @@ class _BookingTicketsState extends State<BookingTickets> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
FlightSelector(
|
FlightSelector(
|
||||||
|
// width: MediaQuery.of(context).size.height * 0.17,
|
||||||
|
widthText: 80.w,
|
||||||
label: 'Penumpang',
|
label: 'Penumpang',
|
||||||
hintText: '1 Dewasa',
|
hintText: '${selectedPassengerCount} Dewasa',
|
||||||
svgIconPath: CustomeIcons.PassengerOutline(),
|
svgIconPath: CustomeIcons.PassengerOutline(),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
|
@ -189,12 +201,25 @@ class _BookingTicketsState extends State<BookingTickets> {
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
),
|
),
|
||||||
SizedBox(height: 20.h),
|
SizedBox(height: 20.h),
|
||||||
ButtonFill(
|
ZoomTapAnimation(
|
||||||
|
child: ButtonFill(
|
||||||
text: 'Cari Tiket',
|
text: 'Cari Tiket',
|
||||||
textColor: Colors.white,
|
textColor: Colors.white,
|
||||||
onTap: () {
|
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() {
|
Widget _buildFlightAddPassenger() {
|
||||||
|
int tempPassengerCount = selectedPassengerCount;
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (context, setModalState) {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
@ -322,13 +350,17 @@ class _BookingTicketsState extends State<BookingTickets> {
|
||||||
Container(
|
Container(
|
||||||
height: 100.h,
|
height: 100.h,
|
||||||
child: CupertinoPicker(
|
child: CupertinoPicker(
|
||||||
scrollController: FixedExtentScrollController(initialItem: 0), // Start at 1
|
scrollController: FixedExtentScrollController(initialItem: tempPassengerCount - 1),
|
||||||
itemExtent: 45.0, // Set the height of each item
|
itemExtent: 45.0,
|
||||||
selectionOverlay: CircleAvatar(
|
selectionOverlay: CircleAvatar(
|
||||||
radius: 18.r,
|
radius: 18.r,
|
||||||
backgroundColor: PrimaryColors.primary800.withOpacity(0.7),
|
backgroundColor: PrimaryColors.primary800.withOpacity(0.7),
|
||||||
),
|
),
|
||||||
onSelectedItemChanged: (index) {},
|
onSelectedItemChanged: (index) {
|
||||||
|
setModalState(() {
|
||||||
|
tempPassengerCount = index + 1;
|
||||||
|
});
|
||||||
|
},
|
||||||
children: List<Widget>.generate(10, (index) {
|
children: List<Widget>.generate(10, (index) {
|
||||||
return Center(
|
return Center(
|
||||||
child: TypographyStyles.body('${index + 1}'),
|
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),
|
SizedBox(height: 20.h),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(bottom: 16.h),
|
padding: EdgeInsets.only(bottom: 16.h),
|
||||||
|
@ -379,12 +375,18 @@ class _BookingTicketsState extends State<BookingTickets> {
|
||||||
child: ButtonFill(
|
child: ButtonFill(
|
||||||
text: 'Selesai',
|
text: 'Selesai',
|
||||||
textColor: Colors.white,
|
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/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import '../../../controllers/ticket_controller.dart';
|
||||||
|
|
||||||
class SearchTicketsScreen extends StatefulWidget {
|
class SearchTicketsScreen extends StatefulWidget {
|
||||||
const SearchTicketsScreen({super.key});
|
const SearchTicketsScreen({super.key});
|
||||||
|
@ -15,15 +18,47 @@ class SearchTicketsScreen extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SearchTicketsScreenState extends State<SearchTicketsScreen> {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final formattedDate = DateFormat('EEE, d MMM yyyy').format(leavingDate);
|
||||||
|
final currencyFormatter = NumberFormat.decimalPattern('id_ID');
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: GrayColors.gray50,
|
backgroundColor: GrayColors.gray50,
|
||||||
appBar: CustomeAppbarComponent(
|
appBar: CustomeAppbarComponent(
|
||||||
valueDari: 'Yogyakarta',
|
valueDari: from,
|
||||||
valueKe: 'Lombok',
|
valueKe: to,
|
||||||
date: 'Sen, 27 Jn 2025',
|
date: '$formattedDate',
|
||||||
passenger: '2',
|
passenger: '$passengerCount',
|
||||||
onTab: () {
|
onTab: () {
|
||||||
Get.back();
|
Get.back();
|
||||||
},
|
},
|
||||||
|
@ -34,33 +69,69 @@ class _SearchTicketsScreenState extends State<SearchTicketsScreen> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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),
|
SizedBox(height: 20.h),
|
||||||
Expanded(
|
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,
|
shrinkWrap: true,
|
||||||
itemCount: 5,
|
itemCount: ticketController.ticketFlight.length,
|
||||||
itemBuilder: (context, index) {
|
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(
|
return Padding(
|
||||||
padding: EdgeInsets.only(bottom: 16.h),
|
padding: EdgeInsets.only(bottom: 16.h),
|
||||||
child: CardTickets(
|
child: CardTickets(
|
||||||
departureCity: 'Yogyakarta (YIA)',
|
departureCity: '${flight.cityDeparture} (${flight.codeDeparture})',
|
||||||
date: 'Sen, 27 Jan',
|
date: ticketDate,
|
||||||
arrivalCity: 'Lombok (LOP)',
|
arrivalCity: '${flight.cityArrival} (${flight.codeArrival})',
|
||||||
departureCode: 'YIA',
|
departureCode: '${flight.codeDeparture}',
|
||||||
arrivalCode: 'LOP',
|
arrivalCode: '${flight.codeArrival}',
|
||||||
departureTime: '12.20 AM',
|
departureTime: departureTime,
|
||||||
arrivalTime: '06.00 AM',
|
arrivalTime: arrivalTime,
|
||||||
duration: '5j 40m',
|
duration: durationFormatted,
|
||||||
seatClass: 'Economy',
|
seatClass: flight.flightClass,
|
||||||
price: 'Rp 1.200.000',
|
price: formattedPrice,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Get.toNamed(Routes.TICKETBOOKINGSTEP1);
|
Get.toNamed(Routes.TICKETBOOKINGSTEP1);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:e_porter/domain/bindings/auth_binding.dart';
|
import 'package:e_porter/domain/bindings/auth_binding.dart';
|
||||||
import 'package:e_porter/domain/bindings/navigation_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/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/forget_password_screen.dart';
|
||||||
import 'package:e_porter/presentation/screens/auth/pages/login_screen.dart';
|
import 'package:e_porter/presentation/screens/auth/pages/login_screen.dart';
|
||||||
import 'package:e_porter/presentation/screens/auth/pages/register_screen.dart';
|
import 'package:e_porter/presentation/screens/auth/pages/register_screen.dart';
|
||||||
|
@ -81,6 +82,7 @@ class AppRoutes {
|
||||||
GetPage(
|
GetPage(
|
||||||
name: Routes.SEARCHTICKETS,
|
name: Routes.SEARCHTICKETS,
|
||||||
page: () => SearchTicketsScreen(),
|
page: () => SearchTicketsScreen(),
|
||||||
|
binding: TicketBinding()
|
||||||
),
|
),
|
||||||
GetPage(
|
GetPage(
|
||||||
name: Routes.TICKETBOOKINGSTEP1,
|
name: Routes.TICKETBOOKINGSTEP1,
|
||||||
|
|
Loading…
Reference in New Issue