From 210209b376e561096bdc8ebf1aa269845eb9c2cc Mon Sep 17 00:00:00 2001 From: orangdeso Date: Tue, 25 Mar 2025 23:49:54 +0700 Subject: [PATCH] Feat: done choose seat and choose service porter --- .dart_tool/package_config.json | 2 +- assets/icons/ic_remove.svg | 3 + lib/_core/component/icons/icons_library.dart | 9 +- .../porter_service_repository_impl.dart | 45 ++ .../bindings/porter_service_binding.dart | 36 ++ lib/domain/models/porter_service_model.dart | 65 +++ lib/domain/models/ticket_model.dart | 4 + lib/domain/models/user_entity.dart | 4 +- .../porter_service_repository.dart | 9 + .../usecases/porter_service_usecase.dart | 20 + .../porter_service_controller.dart | 52 ++ .../screens/home/component/card_seat.dart | 51 +- .../screens/home/component/footer_price.dart | 2 +- .../screens/home/component/porter_radio.dart | 68 +++ .../home/pages/choose_seat_screen.dart | 486 +++++++++++------- .../pages/ticket_booking_step1_screen.dart | 268 ++++++---- .../pages/ticket_booking_step2_screen.dart | 134 ++++- .../pages/ticket_booking_step3_screen.dart | 327 ++++++++++-- .../screens/profile/pages/profile_screen.dart | 211 +++++--- .../screens/routes/app_rountes.dart | 2 + pubspec.yaml | 2 +- 21 files changed, 1381 insertions(+), 419 deletions(-) create mode 100644 assets/icons/ic_remove.svg create mode 100644 lib/data/repositories/porter_service_repository_impl.dart create mode 100644 lib/domain/bindings/porter_service_binding.dart create mode 100644 lib/domain/models/porter_service_model.dart create mode 100644 lib/domain/repositories/porter_service_repository.dart create mode 100644 lib/domain/usecases/porter_service_usecase.dart create mode 100644 lib/presentation/controllers/porter_service_controller.dart create mode 100644 lib/presentation/screens/home/component/porter_radio.dart diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json index d540ae2..aedcc9f 100644 --- a/.dart_tool/package_config.json +++ b/.dart_tool/package_config.json @@ -500,7 +500,7 @@ "languageVersion": "3.4" } ], - "generated": "2025-03-18T17:25:46.276961Z", + "generated": "2025-03-21T19:39:10.352546Z", "generator": "pub", "generatorVersion": "3.5.0", "flutterRoot": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0", diff --git a/assets/icons/ic_remove.svg b/assets/icons/ic_remove.svg new file mode 100644 index 0000000..11f2a30 --- /dev/null +++ b/assets/icons/ic_remove.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/_core/component/icons/icons_library.dart b/lib/_core/component/icons/icons_library.dart index b6df81c..cb7ba16 100644 --- a/lib/_core/component/icons/icons_library.dart +++ b/lib/_core/component/icons/icons_library.dart @@ -47,11 +47,12 @@ class CustomeIcons { static SvgPicture PlusOutline({double? size, Color? color}) => getIcon('ic_plus', color: color); static SvgPicture RefreshOutline({double? size, Color? color}) => getIcon('ic_refresh', color: color); static SvgPicture RunningOutline({double? size, Color? color}) => getIcon('ic_running', color: color); - static SvgPicture VIPFilled({double? size, Color? color}) => getIcon('ic_vip', color: color); - static SvgPicture LockFilled({double? size, Color? color}) => getIcon('ic_lock', color: color); - static SvgPicture AddUserFemaleFilled({double? size, Color? color}) => getIcon('ic_add_user_female', color: color); - static SvgPicture LogoutFilled({double? size, Color? color}) => getIcon('ic_logout', color: color); + static SvgPicture LockOutline({double? size, Color? color}) => getIcon('ic_lock', color: color); + static SvgPicture AddUserFemaleOutline({double? size, Color? color}) => getIcon('ic_add_user_female', color: color); + static SvgPicture LogoutOutline({double? size, Color? color}) => getIcon('ic_logout', color: color); + static SvgPicture RemoveOutline({double? size, Color? color}) => getIcon('ic_remove', color: color); static SvgPicture FlightSeatFilled({double? size, Color? color}) => getIcon('ic_flight_seat_filled', color: color); static SvgPicture PlaneRightFilled({double? size, Color? color}) => getIcon('ic_plane_filled', color: color); + static SvgPicture VIPFilled({double? size, Color? color}) => getIcon('ic_vip', color: color); } diff --git a/lib/data/repositories/porter_service_repository_impl.dart b/lib/data/repositories/porter_service_repository_impl.dart new file mode 100644 index 0000000..1d62184 --- /dev/null +++ b/lib/data/repositories/porter_service_repository_impl.dart @@ -0,0 +1,45 @@ +// data/repositories/porter_service_repository_impl.dart + +import 'package:cloud_firestore/cloud_firestore.dart'; +import '../../domain/models/porter_service_model.dart'; +import '../../domain/repositories/porter_service_repository.dart'; + +class PorterServiceRepositoryImpl implements PorterServiceRepository { + final FirebaseFirestore _firestore; + final String _collectionName = 'PorterServices'; + + PorterServiceRepositoryImpl({FirebaseFirestore? firestore}) + : _firestore = firestore ?? FirebaseFirestore.instance; + + @override + Future> getAllServices() async { + try { + final snapshot = await _firestore.collection(_collectionName).get(); + final services = snapshot.docs.map((doc) { + final data = doc.data() as Map; + return PorterServiceModel.fromJson(data, doc.id); + }).toList(); + return services; + } catch (e) { + throw Exception('Error mengambil daftar layanan porter: $e'); + } + } + + @override + Future> getServicesByType(String type) async { + try { + final snapshot = await _firestore + .collection(_collectionName) + .where('availableFor', arrayContains: type) + .orderBy('sort') + .get(); + + return snapshot.docs.map((doc) { + final data = doc.data() as Map; + return PorterServiceModel.fromJson(data, doc.id); + }).toList(); + } catch (e) { + throw Exception('Error mendapatkan layanan berdasarkan tipe: $e'); + } + } +} diff --git a/lib/domain/bindings/porter_service_binding.dart b/lib/domain/bindings/porter_service_binding.dart new file mode 100644 index 0000000..75a3270 --- /dev/null +++ b/lib/domain/bindings/porter_service_binding.dart @@ -0,0 +1,36 @@ +// domain/bindings/porter_service_binding.dart + +import 'package:get/get.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; + +import '../../domain/repositories/porter_service_repository.dart'; +import '../../data/repositories/porter_service_repository_impl.dart'; +import '../../domain/usecases/porter_service_usecase.dart'; +import '../../presentation/controllers/porter_service_controller.dart'; + +class PorterServiceBinding extends Bindings { + @override + void dependencies() { + // Inisialisasi Firestore instance + final firestore = FirebaseFirestore.instance; + + // Repository - Injeksi implementasi ke abstraksi + Get.lazyPut( + () => PorterServiceRepositoryImpl(firestore: firestore), + ); + + // UseCase + Get.lazyPut( + () => PorterServiceUseCase( + Get.find(), + ), + ); + + // Controller + Get.lazyPut( + () => PorterServiceController( + Get.find(), + ), + ); + } +} \ No newline at end of file diff --git a/lib/domain/models/porter_service_model.dart b/lib/domain/models/porter_service_model.dart new file mode 100644 index 0000000..d583bf1 --- /dev/null +++ b/lib/domain/models/porter_service_model.dart @@ -0,0 +1,65 @@ +class PorterServiceModel { + final String? id; + final String name; + final int price; + final String description; + final List tersediaUntuk; + final int sort; + + PorterServiceModel({ + this.id, + required this.name, + required this.price, + required this.description, + required this.tersediaUntuk, + required this.sort, + }); + + factory PorterServiceModel.fromJson(Map json, [String? docId]) { + List parseTersediaUntuk(dynamic tersediaUntuk) { + if (tersediaUntuk is List) { + return tersediaUntuk.map((item) => item.toString()).toList(); + } else if (tersediaUntuk is Map) { + return tersediaUntuk.entries.map((entry) => entry.value.toString()).toList(); + } + return []; + } + + return PorterServiceModel( + id: docId ?? json['id'], + name: json['name'] ?? '', + price: json['price'] ?? 0, + description: json['description'] ?? '', + tersediaUntuk: parseTersediaUntuk(json['availableFor']), + sort: json['sort'] ?? 0, + ); + } + + Map toJson() { + return { + 'name': name, + 'price': price, + 'description': description, + 'availableFor': tersediaUntuk, + 'sort': sort, + }; + } + + PorterServiceModel copyWith({ + String? id, + String? name, + int? price, + String? description, + List? tersediaUntuk, + int? sort, + }) { + return PorterServiceModel( + id: id ?? this.id, + name: name ?? this.name, + price: price ?? this.price, + description: description ?? this.description, + tersediaUntuk: tersediaUntuk ?? this.tersediaUntuk, + sort: sort ?? this.sort, + ); + } +} diff --git a/lib/domain/models/ticket_model.dart b/lib/domain/models/ticket_model.dart index dc838bd..07bad69 100644 --- a/lib/domain/models/ticket_model.dart +++ b/lib/domain/models/ticket_model.dart @@ -42,6 +42,7 @@ class FlightModel { final String cityDeparture; final String cityArrival; final String codeDeparture; + final String codeTransit; final String codeArrival; final DateTime departureTime; final DateTime arrivalTime; @@ -58,6 +59,7 @@ class FlightModel { required this.cityDeparture, required this.cityArrival, required this.codeDeparture, + required this.codeTransit, required this.codeArrival, required this.departureTime, required this.arrivalTime, @@ -91,6 +93,7 @@ class FlightModel { cityDeparture: data['cityDeparture'] ?? '', cityArrival: data['cityArrival'] ?? '', codeDeparture: data['codeDeparture'] ?? '', + codeTransit: data['codeTransit'] ?? '', codeArrival: data['codeArrival'] ?? '', departureTime: (data['dateDeparture'] as Timestamp).toDate(), arrivalTime: (data['dateArrival'] as Timestamp).toDate(), @@ -109,6 +112,7 @@ class FlightModel { 'cityDeparture': cityDeparture, 'cityArrival': cityArrival, 'codeDeparture': codeDeparture, + 'codeTransit': codeTransit, 'codeArrival': codeArrival, 'dateDeparture': Timestamp.fromDate(departureTime), 'dateArrival': Timestamp.fromDate(arrivalTime), diff --git a/lib/domain/models/user_entity.dart b/lib/domain/models/user_entity.dart index b76a3a4..a74e478 100644 --- a/lib/domain/models/user_entity.dart +++ b/lib/domain/models/user_entity.dart @@ -53,7 +53,7 @@ class UserData { return UserData( uid: map['uid'] ?? '', - tipeId: map['tipeId'] ?? '', + tipeId: map['typeId'] ?? '', noId: map['noId'] ?? '', name: map['name'] as String?, email: map['email'] as String?, @@ -70,7 +70,7 @@ class UserData { Map toMap() { return { 'uid': uid, - 'tipeId': tipeId, + 'typeId': tipeId, 'noId': noId, 'name': name, 'email': email, diff --git a/lib/domain/repositories/porter_service_repository.dart b/lib/domain/repositories/porter_service_repository.dart new file mode 100644 index 0000000..0e7e63b --- /dev/null +++ b/lib/domain/repositories/porter_service_repository.dart @@ -0,0 +1,9 @@ +import '../models/porter_service_model.dart'; + +abstract class PorterServiceRepository { + // Mendapatkan semua layanan porter + Future> getAllServices(); + + // Mendapatkan layanan porter berdasarkan tipe (departure, arrival, transit) + Future> getServicesByType(String type); +} \ No newline at end of file diff --git a/lib/domain/usecases/porter_service_usecase.dart b/lib/domain/usecases/porter_service_usecase.dart new file mode 100644 index 0000000..41c2971 --- /dev/null +++ b/lib/domain/usecases/porter_service_usecase.dart @@ -0,0 +1,20 @@ +// domain/usecases/porter_service_usecase.dart + +import '../models/porter_service_model.dart'; +import '../repositories/porter_service_repository.dart'; + +class PorterServiceUseCase { + final PorterServiceRepository _repository; + + PorterServiceUseCase(this._repository); + + Future> getAllServices() async { + return await _repository.getAllServices(); + } + + Future> getAllServicesOrderedByUrutan() async { + final services = await _repository.getAllServices(); + services.sort((a, b) => a.sort.compareTo(b.sort)); + return services; + } +} \ No newline at end of file diff --git a/lib/presentation/controllers/porter_service_controller.dart b/lib/presentation/controllers/porter_service_controller.dart new file mode 100644 index 0000000..e1966c7 --- /dev/null +++ b/lib/presentation/controllers/porter_service_controller.dart @@ -0,0 +1,52 @@ +// presentation/controllers/porter_service_controller.dart + +import 'package:e_porter/_core/service/logger_service.dart'; +import 'package:get/get.dart'; +import '../../domain/models/porter_service_model.dart'; +import '../../domain/usecases/porter_service_usecase.dart'; + +class PorterServiceController extends GetxController { + final PorterServiceUseCase _porterServiceUseCase; + + PorterServiceController(this._porterServiceUseCase); + + final RxList layananPorter = [].obs; + final RxList layananPorterArrival = [].obs; + final RxList layananPorterDeparture = [].obs; + final RxList layananPorterTransit = [].obs; + + final RxBool isLoading = false.obs; + final RxBool hasError = false.obs; + final RxString pesanError = ''.obs; + final RxString tipeTerpilih = 'semua'.obs; + + @override + void onInit() { + super.onInit(); + fetchLayananPorter(); + } + + Future fetchLayananPorter() async { + try { + isLoading.value = true; + hasError.value = false; + pesanError.value = ''; + + final result = await _porterServiceUseCase.getAllServicesOrderedByUrutan(); + + layananPorterArrival.assignAll(result.where((service) => service.tersediaUntuk.contains('departure')).toList()); + layananPorterDeparture.assignAll(result.where((service) => service.tersediaUntuk.contains('arrival')).toList()); + layananPorterTransit.assignAll(result.where((service) => service.tersediaUntuk.contains('transit')).toList()); + } catch (e) { + logger.e('Error mendapatkan layanan porter: $e'); + hasError.value = true; + pesanError.value = 'Terjadi kesalahan saat memuat layanan porter.'; + } finally { + isLoading.value = false; + } + } + + bool isTipeAktif(String tipe) { + return tipe == 'arrival' || tipe == 'departure' || tipe == 'transit'; + } +} diff --git a/lib/presentation/screens/home/component/card_seat.dart b/lib/presentation/screens/home/component/card_seat.dart index 640645b..ce140bd 100644 --- a/lib/presentation/screens/home/component/card_seat.dart +++ b/lib/presentation/screens/home/component/card_seat.dart @@ -1,32 +1,53 @@ +import 'package:e_porter/_core/constants/typography.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:zoom_tap_animation/zoom_tap_animation.dart'; +import '../../../../_core/constants/colors.dart'; + class CardSeat extends StatelessWidget { - // final VoidCallback onTap; + final bool isTaken; + final bool isSelected; + final String seatLabel; + final VoidCallback? onTap; const CardSeat({ Key? key, - // required this.onTap, + required this.isTaken, + required this.isSelected, + required this.seatLabel, + this.onTap, }); @override Widget build(BuildContext context) { - return Row( - children: [ - ZoomTapAnimation( - child: GestureDetector( - child: Container( - width: 32.w, - height: 32.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6.r), - color: Color(0xFFD9D9D9), - ), - ), + Color backgroundColor; + Border? border; + if (isTaken) { + backgroundColor = const Color(0xFFD9D9D9); + } else if (isSelected) { + backgroundColor = PrimaryColors.primary800; + } else { + backgroundColor = Colors.white; + border = Border.all(width: 1.w, color: PrimaryColors.primary800); + } + + return ZoomTapAnimation( + child: GestureDetector( + onTap: onTap, + child: Container( + width: 40.w, + height: 40.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.r), + border: border, + color: backgroundColor, + ), + child: Center( + child: TypographyStyles.body(seatLabel, color: Colors.white, fontWeight: FontWeight.w500), ), ), - ], + ), ); } } diff --git a/lib/presentation/screens/home/component/footer_price.dart b/lib/presentation/screens/home/component/footer_price.dart index cbfc142..9d89973 100644 --- a/lib/presentation/screens/home/component/footer_price.dart +++ b/lib/presentation/screens/home/component/footer_price.dart @@ -41,7 +41,7 @@ class FooterPrice extends StatelessWidget { ), children: [ TextSpan( - text: "Rp ${price}", + text: price, style: TextStyle( fontFamily: 'DMSans', color: PrimaryColors.primary800, diff --git a/lib/presentation/screens/home/component/porter_radio.dart b/lib/presentation/screens/home/component/porter_radio.dart new file mode 100644 index 0000000..d773d05 --- /dev/null +++ b/lib/presentation/screens/home/component/porter_radio.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../../../_core/constants/colors.dart'; +import '../../../../_core/constants/typography.dart'; + +class PorterRadio extends StatelessWidget { + final String title; + final String subTitle; + final String price; + final String value; + final String groupValue; + final ValueChanged onTap; + + const PorterRadio({ + Key? key, + required this.title, + required this.subTitle, + required this.price, + required this.value, + required this.groupValue, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => onTap, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), + child: Row( + children: [ + Radio( + value: value, + groupValue: groupValue, + onChanged: onTap, + activeColor: PrimaryColors.primary800, + ), + SizedBox(width: 16.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TypographyStyles.body( + title, + color: GrayColors.gray800, + maxlines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 2.h), + TypographyStyles.caption( + subTitle, + color: GrayColors.gray500, + fontWeight: FontWeight.w400, + maxlines: 2, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 10.h), + TypographyStyles.caption(price, color: PrimaryColors.primary800) + ], + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/presentation/screens/home/pages/choose_seat_screen.dart b/lib/presentation/screens/home/pages/choose_seat_screen.dart index 281d6a8..f3a4f3b 100644 --- a/lib/presentation/screens/home/pages/choose_seat_screen.dart +++ b/lib/presentation/screens/home/pages/choose_seat_screen.dart @@ -1,8 +1,13 @@ +// ignore_for_file: unnecessary_null_comparison + import 'package:e_porter/_core/component/appbar/appbar_component.dart'; import 'package:e_porter/_core/component/card/custome_shadow_cotainner.dart'; import 'package:e_porter/_core/component/icons/icons_library.dart'; import 'package:e_porter/_core/constants/colors.dart'; import 'package:e_porter/_core/constants/typography.dart'; +import 'package:e_porter/_core/service/logger_service.dart'; +import 'package:e_porter/domain/models/ticket_model.dart'; +import 'package:e_porter/presentation/controllers/ticket_controller.dart'; import 'package:e_porter/presentation/screens/home/component/card_indicator.dart'; import 'package:e_porter/presentation/screens/home/component/card_seat.dart'; import 'package:flutter/material.dart'; @@ -12,7 +17,7 @@ import 'package:get/get.dart'; import 'package:zoom_tap_animation/zoom_tap_animation.dart'; import '../../../../_core/component/button/button_fill.dart'; -import '../../routes/app_rountes.dart'; +import '../../../../domain/models/user_entity.dart'; class ChooseSeatScreen extends StatefulWidget { const ChooseSeatScreen({super.key}); @@ -23,19 +28,57 @@ class ChooseSeatScreen extends StatefulWidget { class _ChooseSeatScreenState extends State { final List selectedSeats = List.generate(3, (_) => false); - // late ScrollController _scrollController; + final Map selectedSeatsMap = {}; + late List selectedSeatNumbers; + int currentPassenger = 0; + late final String ticketId; + late final String flightId; + late Future _flightFuture; + late final int passenger; + late final List selectedPassengers; + late final TicketController ticketController; - // @override - // void initState() { - // super.initState(); - // _scrollController = ScrollController(); - // } + String? cityDeparture; + String? cityArrival; - // @override - // void dispose() { - // _scrollController.dispose(); - // super.dispose(); - // } + @override + void initState() { + super.initState(); + final args = Get.arguments as Map; + ticketId = args['ticketId']; + flightId = args['flightId']; + passenger = args['passenger'] ?? ''; + selectedPassengers = args['selectedPassenger'] ?? []; + + ticketController = Get.find(); + _flightFuture = ticketController.getFlightById(ticketId: ticketId, flightId: flightId); + + _loadFlightData(); + + selectedSeatNumbers = List.filled(passenger, ''); + if (passenger > 0) { + currentPassenger = 0; + for (int i = 0; i < selectedSeats.length; i++) { + selectedSeats[i] = false; + } + selectedSeats[0] = true; + } + } + + void _loadFlightData() async { + try { + final flight = await ticketController.getFlightById(ticketId: ticketId, flightId: flightId); + if (mounted) { + setState(() { + cityDeparture = flight.cityDeparture; + cityArrival = flight.cityArrival; + _flightFuture = Future.value(flight); + }); + } + } catch (e) { + logger.e('Error loading flight data: $e'); + } + } @override Widget build(BuildContext context) { @@ -44,99 +87,174 @@ class _ChooseSeatScreenState extends State { appBar: CustomeAppbarComponent( valueDari: 'Pilih Kursi', valueKe: null, - date: 'Yogyakarta - Lombok', - passenger: '2', + date: '$cityDeparture - $cityArrival', + passenger: '$passenger', onTab: () { Get.back(); }, ), - body: SafeArea( - child: CustomScrollView( - // controller: _scrollController, - slivers: [ - SliverAppBar( - floating: true, - snap: true, - backgroundColor: GrayColors.gray50, - foregroundColor: Colors.white, - surfaceTintColor: Colors.white, - expandedHeight: 220.h, - flexibleSpace: FlexibleSpaceBar( - background: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 0.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildCardFlight("YIA", "LOP", "Economy", "5j 40m"), - SizedBox(height: 20.h), - SizedBox( - height: 84.h, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w), - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: 3, - itemBuilder: (context, index) { - return Padding( - padding: EdgeInsets.only(right: 16.w), - child: _buildPassengerCard('Ahmad', 'Economy', '10F', index), - ); - }, + body: FutureBuilder( + future: _flightFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text("Error: ${snapshot.error}")); + } else if (!snapshot.hasData) { + return Center(child: Text("Data tidak ditemukan")); + } + + final flight = snapshot.data!; + final codeDeparture = flight.codeDeparture; + final codeArrival = flight.codeArrival; + final codeTransit = flight.codeTransit; + final flightClass = flight.flightClass; + cityDeparture = flight.cityDeparture; + cityArrival = flight.cityArrival; + + 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 duration = '${hours}j ${minutes}m'; + + String stopText = ''; + if (flight.stop != null && flight.stop.isNotEmpty) { + stopText = '${flight.stop} - '; + } + final finalDuration = '$stopText$duration'; + + // Ambil data seat per kolom + final seatInfoA = flight.seat['a']; + final seatInfoB = flight.seat['b']; + final seatInfoC = flight.seat['c']; + final seatInfoD = flight.seat['d']; + final seatInfoE = flight.seat['e']; + final seatInfoF = flight.seat['f']; + + // Ambil totalSeat per kolom + final rowCountA = seatInfoA?.totalSeat ?? 0; + final rowCountB = seatInfoB?.totalSeat ?? 0; + final rowCountC = seatInfoC?.totalSeat ?? 0; + final rowCountD = seatInfoD?.totalSeat ?? 0; + final rowCountE = seatInfoE?.totalSeat ?? 0; + final rowCountF = seatInfoF?.totalSeat ?? 0; + + // Ambil isTaken per kolom + final seatA = seatInfoA?.isTaken ?? []; + final seatB = seatInfoB?.isTaken ?? []; + final seatC = seatInfoC?.isTaken ?? []; + final seatD = seatInfoD?.isTaken ?? []; + final seatE = seatInfoE?.isTaken ?? []; + final seatF = seatInfoF?.isTaken ?? []; + + // Jika ingin menentukan maxRows untuk nomor baris di tengah (misalnya) + final int maxRows = [ + rowCountA, + rowCountB, + rowCountC, + rowCountD, + rowCountE, + rowCountF, + ].reduce((a, b) => a > b ? a : b); + + return SafeArea( + child: CustomScrollView( + slivers: [ + SliverAppBar( + floating: true, + snap: true, + backgroundColor: GrayColors.gray50, + foregroundColor: Colors.white, + surfaceTintColor: Colors.white, + expandedHeight: 220.h, + flexibleSpace: FlexibleSpaceBar( + background: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildCardFlight(codeDeparture, codeTransit, codeArrival, flightClass, finalDuration), + SizedBox(height: 20.h), + SizedBox( + height: 84.h, + child: Padding( + padding: EdgeInsets.only(left: 16.w), + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: selectedPassengers.length, + itemBuilder: (context, index) { + final passengerData = selectedPassengers[index]; + return Padding( + padding: EdgeInsets.only(right: 16.w), + child: _buildPassengerCard( + passengerData?.name ?? "Penumpang ${index + 1}", + flightClass, + '', + index, + ), + ); + }, + ), + ), ), - ), + ], ), - ], + ), ), ), - ), + SliverPersistentHeader( + pinned: true, + delegate: _SliverHeaderDelegate( + minHeight: 50.h, + maxHeight: 50.h, + child: Container( + color: Colors.white, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), + alignment: Alignment.centerLeft, + child: _buildCardInformationStatus(), + ), + ), + ), + SliverToBoxAdapter( + child: CustomeShadowCotainner( + sizeRadius: 0.r, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildSeatColumn("A", seatA, rowCountA), + _buildSeatColumn("B", seatB, rowCountB), + _buildSeatColumn("C", seatC, rowCountC), + SizedBox(width: 16.w), + _buildRowNumbers(maxRows), + SizedBox(width: 16.w), + _buildSeatColumn("D", seatD, rowCountD), + _buildSeatColumn("E", seatE, rowCountE), + _buildSeatColumn("F", seatF, rowCountF), + ], + ), + ), + ), + ], ), - ), - SliverPersistentHeader( - pinned: true, - delegate: _SliverHeaderDelegate( - minHeight: 50.h, - maxHeight: 50.h, - child: Container( - color: Colors.white, - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), - alignment: Alignment.centerLeft, - child: _buildCardInformationStatus(), - ), - ), - ), - SliverToBoxAdapter( - child: CustomeShadowCotainner( - sizeRadius: 0.r, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildCardSeat(context, label: 'A'), - _buildCardSeat(context, label: 'B'), - _buildCardSeat(context, label: 'C'), - SizedBox(width: 16.w), - _buildNumberSeat(context, label: '1'), - _buildCardSeat(context, label: 'D'), - _buildCardSeat(context, label: 'E'), - _buildCardSeat(context, label: 'F'), - ], - ) - ], - ), - ), - ), - ], - )), + ); + }, + ), bottomNavigationBar: CustomeShadowCotainner( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h), child: ZoomTapAnimation( child: ButtonFill( text: 'Lanjutkan', textColor: Colors.white, + backgroundColor: + selectedSeatNumbers.any((seat) => seat.isEmpty) ? GrayColors.gray400 : PrimaryColors.primary800, onTap: () { - Get.toNamed(Routes.TICKETBOOKINGSTEP2); + if (selectedSeatNumbers.any((seat) => seat.isEmpty)) { + return; + } + logger.d('Kursi: $selectedSeatNumbers'); + Get.back(result: selectedSeatNumbers); }, ), ), @@ -144,7 +262,7 @@ class _ChooseSeatScreenState extends State { ); } - Widget _buildCardFlight(String departureCode, String arrivalCode, String kelas, String duration) { + Widget _buildCardFlight(String departureCode, String transitCode, String arrivalCode, String kelas, String duration) { return Padding( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 20.h), child: CustomeShadowCotainner( @@ -157,31 +275,27 @@ class _ChooseSeatScreenState extends State { children: [ Row( children: [ - TypographyStyles.body(departureCode, color: GrayColors.gray800, letterSpacing: 0.2), + TypographyStyles.body(departureCode, color: GrayColors.gray800), SizedBox(width: 10.w), CustomeIcons.RightOutline(color: GrayColors.gray800, size: 14), SizedBox(width: 10.w), - TypographyStyles.body(arrivalCode, color: GrayColors.gray800, letterSpacing: 0.2), + if (transitCode != null && transitCode.isNotEmpty) ...[ + TypographyStyles.body(transitCode, color: GrayColors.gray800), + SizedBox(width: 10.w), + CustomeIcons.RightOutline(color: GrayColors.gray800, size: 14), + SizedBox(width: 10.w), + ], + TypographyStyles.body(arrivalCode, color: GrayColors.gray800), ], ), SizedBox(height: 4.h), Row( children: [ - TypographyStyles.small( - kelas, - color: GrayColors.gray600, - letterSpacing: 0.2, - fontWeight: FontWeight.w400, - ), + TypographyStyles.small(kelas, color: GrayColors.gray600, fontWeight: FontWeight.w400), SizedBox(width: 10.w), CircleAvatar(radius: 2.r, backgroundColor: Color(0xFFD9D9D9)), SizedBox(width: 10.w), - TypographyStyles.small( - duration, - color: GrayColors.gray600, - letterSpacing: 0.2, - fontWeight: FontWeight.w400, - ), + TypographyStyles.small(duration, color: GrayColors.gray600, fontWeight: FontWeight.w400), ], ), ], @@ -201,6 +315,7 @@ class _ChooseSeatScreenState extends State { selectedSeats[i] = false; } selectedSeats[index] = true; + currentPassenger = index; }); }, child: Container( @@ -223,7 +338,7 @@ class _ChooseSeatScreenState extends State { ), SizedBox(height: 2.h), TypographyStyles.caption( - '${kelas} / Kursi ${seat}', + '${kelas} / Kursi ${selectedSeatNumbers[index].isEmpty ? '-' : selectedSeatNumbers[index]}', color: GrayColors.gray600, fontWeight: FontWeight.w400, ), @@ -257,59 +372,82 @@ class _ChooseSeatScreenState extends State { ); } - Widget _buildCardSeat( - BuildContext context, { - required label, - }) { - return Expanded( - child: Column( - children: [ - Container( - width: 32.w, - height: 32.h, - child: TypographyStyles.body(label, color: GrayColors.gray800, fontWeight: FontWeight.w500), - ), - SizedBox(height: 6.h), - ListView.builder( - itemCount: 20, - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - return Padding( - padding: EdgeInsets.symmetric(vertical: 6.h), - child: CardSeat(), - ); - }, - ), - ], - ), - ); - } + // Widget _buildCardSeat( + // BuildContext context, { + // required label, + // }) { + // return Expanded( + // child: Column( + // children: [ + // Container( + // width: 32.w, + // height: 32.h, + // child: TypographyStyles.body(label, color: GrayColors.gray800, fontWeight: FontWeight.w500), + // ), + // SizedBox(height: 6.h), + // ListView.builder( + // itemCount: 20, + // shrinkWrap: true, + // physics: NeverScrollableScrollPhysics(), + // itemBuilder: (context, index) { + // return Padding( + // padding: EdgeInsets.symmetric(vertical: 6.h), + // child: CardSeat(), + // ); + // }, + // ), + // ], + // ), + // ); + // } - Widget _buildNumberSeat( - BuildContext context, { - required label, - }) { + Widget _buildSeatColumn(String column, List seatList, int totalSeat) { return Expanded( child: Column( children: [ Container( width: 32.w, height: 32.h, - child: TypographyStyles.body('', color: Colors.white, fontWeight: FontWeight.w500), + alignment: Alignment.center, + child: TypographyStyles.body( + column, + color: GrayColors.gray800, + fontWeight: FontWeight.w500, + ), ), SizedBox(height: 6.h), ListView.builder( - itemCount: 20, - shrinkWrap: true, + itemCount: totalSeat, physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, itemBuilder: (context, index) { + final seatKey = '$column${index + 1}'; + bool taken = false; + if (index < seatList.length) { + taken = seatList[index]; + } + bool isSelected = selectedSeatsMap[seatKey] ?? false; return Padding( - padding: EdgeInsets.symmetric(vertical: 6.h), - child: Container( - width: 32.w, - height: 32.h, - child: TypographyStyles.body(label, color: GrayColors.gray800, fontWeight: FontWeight.w500), + padding: EdgeInsets.only(top: 6.h, bottom: 6.h, right: 10.w), + child: GestureDetector( + onTap: () { + if (!taken) { + setState(() { + final oldSeatKey = selectedSeatNumbers[currentPassenger]; + if (oldSeatKey.isNotEmpty) { + selectedSeatsMap[oldSeatKey] = false; + } + + selectedSeatsMap[seatKey] = true; + selectedSeatNumbers[currentPassenger] = seatKey; + }); + } + }, + child: CardSeat( + isTaken: taken, + isSelected: isSelected, + seatLabel: seatKey, + ), ), ); }, @@ -319,46 +457,26 @@ class _ChooseSeatScreenState extends State { ); } - Widget _buildNumber() { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 32.w, - height: 32.h, - child: TypographyStyles.body('A', color: GrayColors.gray800, fontWeight: FontWeight.w500), + Widget _buildRowNumbers(int total) { + return Expanded( + child: Column( + children: List.generate( + total, + (index) => Padding( + padding: EdgeInsets.symmetric(vertical: 6.h), + child: Container( + width: 40.w, + height: 40.h, + alignment: Alignment.center, + child: TypographyStyles.body( + '${index + 1}', + color: GrayColors.gray800, + fontWeight: FontWeight.w500, + ), + ), + ), ), - SizedBox(width: 10.w), - Container( - width: 32.w, - height: 32.h, - child: TypographyStyles.body('B', color: GrayColors.gray800, fontWeight: FontWeight.w500), - ), - SizedBox(width: 10.w), - Container( - width: 32.w, - height: 32.h, - child: TypographyStyles.body('C', color: GrayColors.gray800, fontWeight: FontWeight.w500), - ), - SizedBox(width: 106.w), - Container( - width: 32.w, - height: 32.h, - child: TypographyStyles.body('D', color: GrayColors.gray800, fontWeight: FontWeight.w500), - ), - SizedBox(width: 10.w), - Container( - width: 32.w, - height: 32.h, - child: TypographyStyles.body('E', color: GrayColors.gray800, fontWeight: FontWeight.w500), - ), - SizedBox(width: 10.w), - Container( - width: 32.w, - height: 32.h, - child: TypographyStyles.body('F', color: GrayColors.gray800, fontWeight: FontWeight.w500), - ), - ], + ), ); } } diff --git a/lib/presentation/screens/home/pages/ticket_booking_step1_screen.dart b/lib/presentation/screens/home/pages/ticket_booking_step1_screen.dart index d49ebe8..9fdfc7b 100644 --- a/lib/presentation/screens/home/pages/ticket_booking_step1_screen.dart +++ b/lib/presentation/screens/home/pages/ticket_booking_step1_screen.dart @@ -45,6 +45,18 @@ class _TicketBookingStep1ScreenState extends State { dynamic _loggedUser; List selectedPassengers = []; + String? departureTime; + String? arrivalTime; + String? cityDeparture; + String? cityArrival; + String? airLines; + String? code; + String? flightClass; + String? transitAirplane; + String? stop; + String? codeDeparture; + String? codeArrival; + @override void initState() { super.initState(); @@ -61,6 +73,15 @@ class _TicketBookingStep1ScreenState extends State { selectedPassengers = List.filled(passenger, null, growable: false); } + PassengerModel _convertUserDataToPassengerModel(UserData userData) { + return PassengerModel( + name: userData.name ?? '', + typeId: userData.tipeId ?? '', + noId: userData.noId ?? '', + gender: userData.gender ?? '', + ); + } + Future _loadPassengers() async { final userData = await PreferencesService.getUserData(); if (userData == null || userData.uid.isEmpty) { @@ -103,8 +124,17 @@ class _TicketBookingStep1ScreenState extends State { } final flight = snapshot.data!; - final departureTime = DateFormat.jm().format(flight.departureTime); - final arrivalTime = DateFormat.jm().format(flight.arrivalTime); + departureTime = DateFormat.jm().format(flight.departureTime); + arrivalTime = DateFormat.jm().format(flight.arrivalTime); + cityDeparture = flight.cityDeparture; + cityArrival = flight.cityArrival; + airLines = flight.airLines; + code = flight.code; + flightClass = flight.flightClass; + transitAirplane = flight.transitAirplane; + stop = flight.stop; + codeDeparture = flight.codeDeparture; + codeArrival = flight.codeArrival; return SafeArea( child: Padding( @@ -116,13 +146,13 @@ class _TicketBookingStep1ScreenState extends State { CardFlightInformation( date: ticketDate, time: '$departureTime - $arrivalTime', - departureCity: flight.cityDeparture, - arrivalCity: flight.cityArrival, - plane: '${flight.airLines} (${flight.code})', - seatClass: flight.flightClass, + departureCity: '$cityDeparture', + arrivalCity: '$cityArrival', + plane: '${airLines} (${code})', + seatClass: '$flightClass', passenger: '$passenger', - transiAirplane: flight.transitAirplane, - stop: flight.stop, + transiAirplane: '$transitAirplane', + stop: '$stop', ), SizedBox(height: 32.h), TypographyStyles.h6('Detail Pemesanan', color: GrayColors.gray800), @@ -138,7 +168,7 @@ class _TicketBookingStep1ScreenState extends State { ), ); }, - ), + ), bottomNavigationBar: CustomeShadowCotainner( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h), child: ButtonFill( @@ -146,10 +176,30 @@ class _TicketBookingStep1ScreenState extends State { textColor: Colors.white, backgroundColor: isAllPassengersFilled() ? PrimaryColors.primary800 : GrayColors.gray400, onTap: () { - if (!isAllPassengersFilled()) { + logger.d('Selected Passengers: $selectedPassengers'); + if (selectedPassengers.any((p) => p == null)) { SnackbarHelper.showError('Error', 'Harap lengkapi slot penumpang'); } else { - Get.toNamed(Routes.TICKETBOOKINGSTEP2); + final argument = { + 'ticketId': ticketId, + 'flightId': flightId, + 'date': ticketDate, + 'departureTime': departureTime, + 'arrivalTime': arrivalTime, + 'cityDeparture': cityDeparture, + 'cityArrival': cityArrival, + 'airLines': airLines, + 'code': code, + 'flightClass': flightClass, + 'transitAirplane': transitAirplane, + 'stop': stop, + 'codeDeparture': codeDeparture, + 'codeArrival': codeArrival, + 'passenger': passenger, + 'selectedPassenger': selectedPassengers, + }; + + Get.toNamed(Routes.TICKETBOOKINGSTEP2, arguments: argument); } }, ), @@ -201,11 +251,14 @@ class _TicketBookingStep1ScreenState extends State { final prefs = await SharedPreferences.getInstance(); await prefs.setBool('isPassengerAdd', newValue); - setState( - () { - isToggled = newValue; - }, - ); + setState(() { + isToggled = newValue; + if (!newValue) { + selectedPassengers[0] = null; + } else if (_loggedUser != null) { + selectedPassengers[0] = _convertUserDataToPassengerModel(_loggedUser); + } + }); }, ) ], @@ -233,7 +286,7 @@ class _TicketBookingStep1ScreenState extends State { passenger, (index) { if (isToggled && index == 0 && _loggedUser != null) { - return _buildUserPassengerCard(_loggedUser); + return _buildUserPassengerCard(_loggedUser, index); } else { final p = selectedPassengers[index]; if (p != null) { @@ -247,7 +300,8 @@ class _TicketBookingStep1ScreenState extends State { ); } - Widget _buildUserPassengerCard(dynamic user) { + Widget _buildUserPassengerCard(dynamic user, int index) { + bool isSlotEditTable = !isToggled || index != 0; return Padding( padding: EdgeInsets.only(bottom: 16.h), child: CustomeShadowCotainner( @@ -270,76 +324,92 @@ class _TicketBookingStep1ScreenState extends State { ) ], ), - ZoomTapAnimation( - child: GestureDetector( - child: CustomeIcons.EditOutline(), - onTap: () { - Get.bottomSheet( - Padding( - padding: EdgeInsets.only(left: 16.w, right: 16.w, bottom: 16.h), - child: Wrap( - children: [ - TitleShowModal( - text: 'Informasi Penumpang', - onTap: () async { - if (Get.isBottomSheetOpen ?? false) { - Get.back(); - } - await Future.delayed(Duration(seconds: 1)); - var result = await Get.toNamed(Routes.ADDPASSENGER); - if (result == true) { - _loadPassengers().then((_) => setState(() {})); - } - }, - ), - Obx( - () { - if (profilController.passengerList.isEmpty) { - return Center( - child: TypographyStyles.body( - "Belum ada penumpang", - color: GrayColors.gray400, - fontWeight: FontWeight.w500, - ), - ); - } - return ListView.builder( - itemCount: profilController.passengerList.length, - shrinkWrap: true, - itemBuilder: (context, index) { - final passenger = profilController.passengerList[index]; - logger.d("Passenger Models : ${passenger.noId}"); - return Padding( - padding: EdgeInsets.only(top: 16.h), - child: _buildAddPassenger( - context, - title: "${passenger.name}", - subTitle: "${passenger.typeId} - ${passenger.noId}", - onTap: () { - selectedPassengers[index] = passenger; - Get.back(); - setState(() {}); + Row( + children: [ + ZoomTapAnimation( + child: IconButton( + icon: CustomeIcons.RemoveOutline(), + onPressed: () { + setState(() { + selectedPassengers[0] = null; + isToggled = false; + }); + }, + ), + ), + if (isSlotEditTable) + ZoomTapAnimation( + child: GestureDetector( + child: CustomeIcons.EditOutline(), + onTap: () { + Get.bottomSheet( + Padding( + padding: EdgeInsets.only(left: 16.w, right: 16.w, bottom: 16.h), + child: Wrap( + children: [ + TitleShowModal( + text: 'Informasi Penumpang', + onTap: () async { + if (Get.isBottomSheetOpen ?? false) { + Get.back(); + } + await Future.delayed(Duration(seconds: 1)); + var result = await Get.toNamed(Routes.ADDPASSENGER); + if (result == true) { + _loadPassengers().then((_) => setState(() {})); + } + }, + ), + Obx( + () { + if (profilController.passengerList.isEmpty) { + return Center( + child: TypographyStyles.body( + "Belum ada penumpang", + color: GrayColors.gray400, + fontWeight: FontWeight.w500, + ), + ); + } + return ListView.builder( + itemCount: profilController.passengerList.length, + shrinkWrap: true, + itemBuilder: (context, index) { + final passenger = profilController.passengerList[index]; + logger.d("Passenger Models : ${passenger.noId}"); + return Padding( + padding: EdgeInsets.only(top: 16.h), + child: _buildAddPassenger( + context, + title: "${passenger.name}", + subTitle: "${passenger.typeId} - ${passenger.noId}", + onTap: () { + selectedPassengers[index] = passenger; + Get.back(); + setState(() {}); + }, + ), + ); }, - ), - ); - }, - ); - }, + ); + }, + ), + ], + ), ), - ], - ), + backgroundColor: Colors.white, + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.r), + topRight: Radius.circular(10.r), + ), + ), + ); + }, ), - backgroundColor: Colors.white, - isScrollControlled: true, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(10.r), - topRight: Radius.circular(10.r), - ), - ), - ); - }, - ), + ), + ], ) ], ), @@ -370,13 +440,27 @@ class _TicketBookingStep1ScreenState extends State { ) ], ), - ZoomTapAnimation( - child: GestureDetector( - child: CustomeIcons.EditOutline(), - onTap: () { - _onEditPassenger(slotIndex); - }, - ), + Row( + children: [ + ZoomTapAnimation( + child: IconButton( + icon: CustomeIcons.RemoveOutline(), + onPressed: () { + setState(() { + selectedPassengers[slotIndex] = null; + }); + }, + ), + ), + ZoomTapAnimation( + child: GestureDetector( + child: CustomeIcons.EditOutline(), + onTap: () { + _onEditPassenger(slotIndex); + }, + ), + ), + ], ) ], ), diff --git a/lib/presentation/screens/home/pages/ticket_booking_step2_screen.dart b/lib/presentation/screens/home/pages/ticket_booking_step2_screen.dart index 3709605..69987ce 100644 --- a/lib/presentation/screens/home/pages/ticket_booking_step2_screen.dart +++ b/lib/presentation/screens/home/pages/ticket_booking_step2_screen.dart @@ -1,6 +1,9 @@ import 'package:e_porter/_core/component/appbar/appbar_component.dart'; import 'package:e_porter/_core/component/card/custome_shadow_cotainner.dart'; import 'package:e_porter/_core/constants/colors.dart'; +import 'package:e_porter/_core/service/logger_service.dart'; +import 'package:e_porter/domain/models/user_entity.dart'; +import 'package:e_porter/presentation/controllers/ticket_controller.dart'; import 'package:e_porter/presentation/screens/routes/app_rountes.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -19,6 +22,50 @@ class TicketBookingStep2Screen extends StatefulWidget { } class _TicketBookingStep2ScreenState extends State { + late final TicketController ticketController; + late final String ticketId; + late final String flightId; + String? ticketDate; + String? departureTime; + String? arrivalTime; + String? cityDeparture; + String? cityArrival; + String? airLines; + String? code; + String? flightClass; + String? transitAirplane; + String? stop; + String? codeDeparture; + String? codeArrival; + late final int passenger; + late final List selectedPassengers; + late List selectedSeatNumbers; + + @override + void initState() { + super.initState(); + final args = Get.arguments as Map; + ticketId = args['ticketId']; + flightId = args['flightId']; + ticketDate = args['date']; + departureTime = args['departureTime']; + arrivalTime = args['arrivalTime']; + cityDeparture = args['cityDeparture']; + cityArrival = args['cityArrival']; + airLines = args['airLines']; + code = args['code']; + flightClass = args['flightClass']; + transitAirplane = args['transitAirplane']; + stop = args['stop']; + codeDeparture = args['codeDeparture']; + codeArrival = args['codeArrival']; + passenger = args['passenger']; + selectedPassengers = args['selectedPassenger'] ?? []; + selectedSeatNumbers = args['selectedSeatNumbers'] ?? List.filled(passenger, ''); + + logger.d('Ticket ID: $ticketId \nFlight ID: $flightId'); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -38,25 +85,47 @@ class _TicketBookingStep2ScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ CardFlightInformation( - date: 'Sen, 27 Jan 2025', - time: '12.20 - 06.00 AM', - departureCity: 'Yogyakarta', - arrivalCity: 'Lombok', - plane: 'Citilink (103)', - seatClass: 'Economy', - passenger: '2', + date: '$ticketDate', + time: '${departureTime} - ${arrivalTime}', + departureCity: '$cityDeparture', + arrivalCity: '$cityArrival', + plane: '${airLines} (${code})', + seatClass: '$flightClass', + passenger: '$passenger', + transiAirplane: '$transitAirplane', + stop: '$stop', ), SizedBox(height: 32.h), TypographyStyles.h6('Pilih Kursi', color: GrayColors.gray800), - SizedBox(height: 20.h), - _buildCardSeatPessenger( - context, - label: '1', - namePassenger: 'AHMAD CHOIRUL UMAM ALI R', - seatClass: 'Economy', - numberSeat: '10F', - onTap: () { - Get.toNamed(Routes.CHOOSECHAIR); + SizedBox(height: 4.h), + ...List.generate( + passenger, + (index) { + final passengerData = selectedPassengers[index]; + return Padding( + padding: EdgeInsets.only(top: 16.h), + child: _buildCardSeatPessenger( + context, + label: '${index + 1}', + namePassenger: passengerData?.name ?? 'Unknown Passenger', + seatClass: '$flightClass', + numberSeat: '${selectedSeatNumbers[index].isEmpty ? '-' : selectedSeatNumbers[index]}', + onTap: () async { + final argument = { + 'ticketId': ticketId, + 'flightId': flightId, + 'passenger': passenger, + 'selectedPassenger': selectedPassengers, + }; + final result = await Get.toNamed(Routes.CHOOSECHAIR, arguments: argument); + if (result != null && result is List) { + setState(() { + selectedSeatNumbers = result; + }); + } + }, + ), + ); }, ), ], @@ -70,21 +139,36 @@ class _TicketBookingStep2ScreenState extends State { child: ButtonFill( text: 'Lanjutkan', textColor: Colors.white, - onTap: () { - Get.toNamed(Routes.TICKETBOOKINGSTEP3); - }, + backgroundColor: + selectedSeatNumbers.any((seat) => seat.isEmpty) ? GrayColors.gray400 : PrimaryColors.primary800, + onTap: selectedSeatNumbers.any((seat) => seat.isEmpty) + ? null + : () { + final argument = { + 'ticketId': ticketId, + 'flightId': flightId, + 'date': ticketDate, + 'passenger': passenger, + 'selectedPassenger': selectedPassengers, + 'numberSeat': selectedSeatNumbers + }; + logger.d('Number Seat: $selectedSeatNumbers \n Passenger: $selectedPassengers'); + Get.toNamed(Routes.TICKETBOOKINGSTEP3, arguments: argument); + }, ), ), ), ); } - Widget _buildCardSeatPessenger(BuildContext context, - {required String label, - required String namePassenger, - required String seatClass, - required String numberSeat, - required VoidCallback onTap}) { + Widget _buildCardSeatPessenger( + BuildContext context, { + required String label, + required String namePassenger, + required String seatClass, + required String numberSeat, + required VoidCallback onTap, + }) { return CustomeShadowCotainner( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/presentation/screens/home/pages/ticket_booking_step3_screen.dart b/lib/presentation/screens/home/pages/ticket_booking_step3_screen.dart index 7af2d3e..200ab7f 100644 --- a/lib/presentation/screens/home/pages/ticket_booking_step3_screen.dart +++ b/lib/presentation/screens/home/pages/ticket_booking_step3_screen.dart @@ -2,12 +2,20 @@ import 'package:e_porter/_core/component/card/custome_shadow_cotainner.dart'; import 'package:e_porter/_core/component/icons/icons_library.dart'; import 'package:e_porter/_core/constants/colors.dart'; import 'package:e_porter/_core/constants/typography.dart'; +import 'package:e_porter/_core/service/logger_service.dart'; import 'package:e_porter/presentation/screens/home/component/footer_price.dart'; +import 'package:e_porter/presentation/screens/home/component/porter_radio.dart'; 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 '../../../../_core/component/appbar/appbar_component.dart'; +import '../../../../domain/models/porter_service_model.dart'; +import '../../../../domain/models/ticket_model.dart'; +import '../../../../domain/models/user_entity.dart'; +import '../../../controllers/porter_service_controller.dart'; +import '../../../controllers/ticket_controller.dart'; import '../component/card_flight_information.dart'; class TicketBookingStep3Screen extends StatefulWidget { @@ -18,12 +26,175 @@ class TicketBookingStep3Screen extends StatefulWidget { } class _TicketBookingStep3ScreenState extends State { + final ValueNotifier selectedPorter1 = ValueNotifier(''); + final ValueNotifier selectedPorter2 = ValueNotifier(''); + final ValueNotifier selectedPorter3 = ValueNotifier(''); + + Map layananTipe = {1: 'departure', 2: 'arrival', 3: 'transit'}; + bool _isChecked1 = false; bool _isChecked2 = false; bool _isChecked3 = false; + late final PorterServiceController _porterController; + final TicketController ticketController = Get.find(); + late final String ticketId; + late final String flightId; + late String? ticketDate; + late final int passenger; + late final List selectedPassengers; + late List numberSeat; + + FlightModel? flightData; + String? departureTime; + String? arrivalTime; + + double totalPriceService = 0.0; + PorterServiceModel? selectedDepartureService; + PorterServiceModel? selectedArrivalService; + PorterServiceModel? selectedTransitService; + + @override + void initState() { + super.initState(); + final args = Get.arguments as Map; + ticketId = args['ticketId']; + flightId = args['flightId']; + ticketDate = args['date']; + passenger = args['passenger'] ?? 0; + selectedPassengers = args['selectedPassenger'] ?? []; + numberSeat = args['numberSeat']; + + _porterController = Get.find(); + _porterController.fetchLayananPorter(); + + fetchDataFlight(); + } + + Future fetchDataFlight() async { + try { + FlightModel flight = await ticketController.getFlightById(ticketId: ticketId, flightId: flightId); + setState(() { + flightData = flight; + departureTime = DateFormat.jm().format(flightData!.departureTime); + arrivalTime = DateFormat.jm().format(flightData!.arrivalTime); + }); + } catch (e) { + logger.e('Terjadi kesalahan: $e'); + } + } + + double calculateTotalPrice(double ticketPrice, int passengerCount) { + return ticketPrice * passengerCount; + } + + void updateServicePrice(double servicePrice, bool isSelected) { + setState(() { + if (isSelected) { + totalPriceService += servicePrice; + } else { + totalPriceService -= servicePrice; + } + }); + } + + void _onCheckboxChanged(int checkboxNumber, bool? value) { + setState(() { + switch (checkboxNumber) { + case 1: + bool oldValue = _isChecked1; + _isChecked1 = value ?? false; + + if (oldValue && !_isChecked1 && selectedDepartureService != null) { + totalPriceService -= selectedDepartureService!.price; + selectedDepartureService = null; + selectedPorter1.value = ''; + } + break; + case 2: + bool oldValue = _isChecked2; + _isChecked2 = value ?? false; + + if (oldValue && !_isChecked2 && selectedArrivalService != null) { + totalPriceService -= selectedArrivalService!.price; + selectedArrivalService = null; + selectedPorter2.value = ''; + } + break; + case 3: + bool oldValue = _isChecked3; + _isChecked3 = value ?? false; + + if (oldValue && !_isChecked3 && selectedTransitService != null) { + totalPriceService -= selectedTransitService!.price; + selectedTransitService = null; + selectedPorter3.value = ''; + } + break; + } + }); + } + + void _onPorterSelectionChanged(PorterServiceModel service, bool isSelected, String serviceType) { + setState(() { + if (serviceType == 'departure') { + if (isSelected) { + if (selectedDepartureService != null) { + totalPriceService -= selectedDepartureService!.price; + } + totalPriceService += service.price; + selectedDepartureService = service; + } else { + if (selectedDepartureService != null) { + totalPriceService -= selectedDepartureService!.price; + selectedDepartureService = null; + } + } + + } else if (serviceType == 'arrival') { + if (isSelected) { + if (selectedArrivalService != null) { + totalPriceService -= selectedArrivalService!.price; + } + totalPriceService += service.price; + selectedArrivalService = service; + } else { + if (selectedArrivalService != null) { + totalPriceService -= selectedArrivalService!.price; + selectedArrivalService = null; + } + } + + } else if (serviceType == 'transit') { + if (isSelected) { + if (selectedTransitService != null) { + totalPriceService -= selectedTransitService!.price; + } + totalPriceService += service.price; + selectedTransitService = service; + } else { + if (selectedTransitService != null) { + totalPriceService -= selectedTransitService!.price; + selectedTransitService = null; + } + } + } + }); + } + + List _getSelectedServices() { + List selectedServices = []; + if (_isChecked1) selectedServices.add("Keberangkatan: Fast Track"); + if (_isChecked2) selectedServices.add("Kedatangan: Porter VIP"); + if (_isChecked3) selectedServices.add("Transit: Transit"); + return selectedServices; + } + @override Widget build(BuildContext context) { + double totalPrice = calculateTotalPrice(flightData?.price.toDouble() ?? 0.0, passenger); + double grandTotal = totalPrice + totalPriceService; + return Scaffold( backgroundColor: GrayColors.gray50, appBar: ProgressAppbarComponent( @@ -41,13 +212,15 @@ class _TicketBookingStep3ScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ CardFlightInformation( - date: "Sen, 27 Jan 2025", - time: "12.20 - 06.00 AM", - departureCity: "Yogyakarta", - arrivalCity: "Lombok", - plane: "Citilink (103)", - seatClass: "Economy", - passenger: "2", + date: '$ticketDate', + time: "${departureTime} - ${arrivalTime}", + departureCity: '${flightData?.cityDeparture}', + arrivalCity: "${flightData?.cityArrival}", + plane: "${flightData?.airLines} (${flightData?.code})", + seatClass: "${flightData?.flightClass}", + passenger: "$passenger", + stop: "${flightData?.stop}", + transiAirplane: "${flightData?.transitAirplane}", ), SizedBox(height: 32.h), TypographyStyles.h6("Layanan Porter", color: GrayColors.gray800), @@ -59,48 +232,50 @@ class _TicketBookingStep3ScreenState extends State { maxlines: 3, ), SizedBox(height: 16.h), - _buildCheckBox( - context, - label: "Kedatangan", - Widget: CustomeIcons.AirplaneLandingOutline(color: Colors.white), - value: _isChecked1, - onTap: (bool? value) { - setState(() { - _isChecked1 = value ?? false; - }); - }, - ), - SizedBox(height: 10.h), _buildCheckBox( context, label: "Keberangkatan", Widget: CustomeIcons.AirplaneTakeOffOutline(color: Colors.white), - value: _isChecked2, + value: _isChecked1, onTap: (bool? value) { - setState(() { - _isChecked2 = value ?? false; - }); + _onCheckboxChanged(1, value); }, ), + if (_isChecked1) + _buildPorterServicesList(selectedPorter: selectedPorter1, serviceType: layananTipe[1]!), SizedBox(height: 10.h), _buildCheckBox( context, - label: "Transit", - Widget: CustomeIcons.TransitOutline(color: Colors.white), - value: _isChecked3, + label: "Kedatangan", + Widget: CustomeIcons.AirplaneLandingOutline(color: Colors.white), + value: _isChecked2, onTap: (bool? value) { - setState(() { - _isChecked3 = value ?? false; - }); + _onCheckboxChanged(2, value); }, ), + if (_isChecked2) + _buildPorterServicesList(selectedPorter: selectedPorter2, serviceType: layananTipe[2]!), + SizedBox(height: 10.h), + if (flightData?.stop != null && flightData!.stop.isNotEmpty) ...[ + _buildCheckBox( + context, + label: "Transit", + Widget: CustomeIcons.TransitOutline(color: Colors.white), + value: _isChecked3, + onTap: (bool? value) { + _onCheckboxChanged(3, value); + }, + ), + if (_isChecked3) + _buildPorterServicesList(selectedPorter: selectedPorter3, serviceType: layananTipe[3]!), + ], ], ), ), ), ), bottomNavigationBar: FooterPrice( - price: "1.450.000", + price: "Rp ${NumberFormat.decimalPattern('id_ID').format(grandTotal)}", labelText: "Pesanan", labelButton: "Lanjut", onTap: () { @@ -141,4 +316,96 @@ class _TicketBookingStep3ScreenState extends State { ), ); } + + Widget _buildPorterServicesList({ + required ValueNotifier selectedPorter, + required String serviceType, + }) { + return Padding( + padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 32.w, right: 0), + child: ValueListenableBuilder( + valueListenable: selectedPorter, + builder: (context, selectedValue, child) { + return Obx(() { + if (_porterController.isLoading.value) { + return Center( + child: CircularProgressIndicator( + color: PrimaryColors.primary800, + ), + ); + } + + if (_porterController.hasError.value) { + return Padding( + padding: EdgeInsets.all(8.h), + child: Text( + 'Terjadi kesalahan: ${_porterController.pesanError.value}', + style: TextStyle(color: Colors.red), + ), + ); + } + + List filteredServices = []; + if (serviceType == 'departure') { + filteredServices = _porterController.layananPorterArrival; + } else if (serviceType == 'arrival') { + filteredServices = _porterController.layananPorterDeparture; + } else if (serviceType == 'transit') { + filteredServices = _porterController.layananPorterTransit; + } + + if (filteredServices.isEmpty) { + return Padding( + padding: EdgeInsets.all(8.h), + child: Text( + 'Tidak ada layanan porter untuk ${_getTipeLabel(serviceType)}', + style: TextStyle(color: GrayColors.gray600), + ), + ); + } + + return ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: filteredServices.length, + itemBuilder: (context, index) { + final service = filteredServices[index]; + return Column( + children: [ + PorterRadio( + title: service.name, + subTitle: service.description, + price: 'Rp ${NumberFormat.decimalPattern('id_ID').format(service.price)}', + value: service.id ?? '', + groupValue: selectedValue, + onTap: (value) { + selectedPorter.value = value!; + _onPorterSelectionChanged(service, selectedPorter.value.isNotEmpty, serviceType); + }, + ), + SizedBox(height: 10.h), + Divider(thickness: 1, color: GrayColors.gray200), + SizedBox(height: 10.h), + ], + ); + }, + ); + }); + }, + ), + ); + } + + String _getTipeLabel(String serviceType) { + switch (serviceType) { + case 'departure': + return 'Keberangkatan'; + case 'arrival': + return 'Kedatangan'; + case 'transit': + return 'Transit'; + default: + return serviceType; + } + } } diff --git a/lib/presentation/screens/profile/pages/profile_screen.dart b/lib/presentation/screens/profile/pages/profile_screen.dart index 03d448a..b11a54d 100644 --- a/lib/presentation/screens/profile/pages/profile_screen.dart +++ b/lib/presentation/screens/profile/pages/profile_screen.dart @@ -5,8 +5,13 @@ import 'package:e_porter/presentation/screens/home/component/profile_avatar.dart import 'package:e_porter/presentation/screens/profile/component/profile_menu.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:zoom_tap_animation/zoom_tap_animation.dart'; import '../../../../_core/constants/colors.dart'; +import '../../../../_core/service/preferences_service.dart'; +import '../../../../domain/models/user_entity.dart'; +import '../../routes/app_rountes.dart'; class ProfileScreen extends StatefulWidget { const ProfileScreen({super.key}); @@ -16,81 +21,159 @@ class ProfileScreen extends StatefulWidget { } class _ProfileScreenState extends State { + late final String role; + late Future _userDataFuture; + + @override + void initState() { + super.initState(); + role = Get.arguments ?? 'penumpang'; + _userDataFuture = PreferencesService.getUserData(); + } + @override Widget build(BuildContext context) { + if (role == 'porter') { + return _buildPorterUI(); + } + return _buildPassengerUI(); + } + + Widget _buildPassengerUI() { return Scaffold( backgroundColor: GrayColors.gray50, appBar: BasicAppbarComponent(title: 'Profil'), - body: SafeArea( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 20.h), - child: SingleChildScrollView( - child: Column( - children: [ - CustomeShadowCotainner( - borderRadius: BorderRadius.circular(0.r), - child: Row( - children: [ - ProfileAvatar(fullName: 'fullName'), - SizedBox(width: 16.w), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + body: FutureBuilder( + future: _userDataFuture, + builder: (context, snapshot) { + String userName = "Guest"; + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } else if (snapshot.hasData && snapshot.data?.name != null) { + userName = snapshot.data!.name!; + } + return SafeArea( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 20.h), + child: SingleChildScrollView( + child: Column( + children: [ + CustomeShadowCotainner( + borderRadius: BorderRadius.circular(0.r), + child: Row( children: [ - TypographyStyles.caption('Hi,', color: GrayColors.gray600, fontWeight: FontWeight.w400), - TypographyStyles.body('Muhammad Al Kahfi', color: GrayColors.gray800), + ProfileAvatar(fullName: userName), + SizedBox(width: 16.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TypographyStyles.caption('Hi,', color: GrayColors.gray600, fontWeight: FontWeight.w400), + TypographyStyles.body(userName, color: GrayColors.gray800), + ], + ), ], ), - ], - ), + ), + SizedBox(height: 20.h), + CustomeShadowCotainner( + borderRadius: BorderRadius.circular(0.r), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TypographyStyles.h6('Pengaturan', color: GrayColors.gray800), + SizedBox(height: 32.h), + ProfileMenu( + label: 'Lihat Profile', + svgIcon: 'assets/icons/ic_profile.svg', + onTap: () {}, + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 20.h), + child: Divider(thickness: 1, color: GrayColors.gray100), + ), + ProfileMenu( + label: 'Ganti Kata Sandi', + svgIcon: 'assets/icons/ic_lock.svg', + onTap: () {}, + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 20.h), + child: Divider(thickness: 1, color: GrayColors.gray100), + ), + ProfileMenu( + label: 'Tambah Penumpang', + svgIcon: 'assets/icons/ic_add_user_female.svg', + onTap: () {}, + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 20.h), + child: Divider(thickness: 1, color: GrayColors.gray100), + ), + ProfileMenu( + label: 'Logout', + svgIcon: 'assets/icons/ic_logout.svg', + onTap: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + backgroundColor: Colors.white, + title: TypographyStyles.body('Keluar', color: GrayColors.gray800), + content: TypographyStyles.caption( + 'Apakah anda yakin untuk keluar dari akun ini?', + color: GrayColors.gray600, + fontWeight: FontWeight.w500, + maxlines: 3, + ), + actions: [ + ZoomTapAnimation( + child: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: TypographyStyles.caption( + 'Tidak', + color: GrayColors.gray800, + ), + ), + ), + ZoomTapAnimation( + child: GestureDetector( + onTap: () async { + await PreferencesService.clearUserData(); + Navigator.of(context).pop(); + Get.offAllNamed(Routes.SPLASH); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 30.w, vertical: 8.h), + decoration: BoxDecoration( + color: PrimaryColors.primary800, + borderRadius: BorderRadius.circular(8.r), + ), + child: TypographyStyles.caption('Ya', color: Colors.white), + ), + ), + ) + ], + ); + }, + ); + }, + ), + ], + ), + ) + ], ), - SizedBox(height: 20.h), - CustomeShadowCotainner( - borderRadius: BorderRadius.circular(0.r), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TypographyStyles.h6('Pengaturan', color: GrayColors.gray800), - SizedBox(height: 32.h), - ProfileMenu( - label: 'Lihat Profile', - svgIcon: 'assets/icons/ic_profile.svg', - onTap: () {}, - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 20.h), - child: Divider(thickness: 1, color: GrayColors.gray100), - ), - ProfileMenu( - label: 'Ganti Kata Sandi', - svgIcon: 'assets/icons/ic_lock.svg', - onTap: () {}, - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 20.h), - child: Divider(thickness: 1, color: GrayColors.gray100), - ), - ProfileMenu( - label: 'Tambah Penumpang', - svgIcon: 'assets/icons/ic_add_user_female.svg', - onTap: () {}, - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 20.h), - child: Divider(thickness: 1, color: GrayColors.gray100), - ), - ProfileMenu( - label: 'Logout', - svgIcon: 'assets/icons/ic_logout.svg', - onTap: () {}, - ), - ], - ), - ) - ], + ), ), - ), - ), + ); + }, ), ); } + + Widget _buildPorterUI() { + return Scaffold(); + } } diff --git a/lib/presentation/screens/routes/app_rountes.dart b/lib/presentation/screens/routes/app_rountes.dart index 5aefc23..f154988 100644 --- a/lib/presentation/screens/routes/app_rountes.dart +++ b/lib/presentation/screens/routes/app_rountes.dart @@ -1,5 +1,6 @@ import 'package:e_porter/domain/bindings/auth_binding.dart'; import 'package:e_porter/domain/bindings/navigation_binding.dart'; +import 'package:e_porter/domain/bindings/porter_service_binding.dart'; import 'package:e_porter/domain/bindings/profil_binding.dart'; import 'package:e_porter/domain/bindings/search_flight_binding.dart'; import 'package:e_porter/domain/bindings/ticket_binding.dart'; @@ -105,6 +106,7 @@ class AppRoutes { GetPage( name: Routes.TICKETBOOKINGSTEP3, page: () => TicketBookingStep3Screen(), + binding: PorterServiceBinding(), ), GetPage( name: Routes.TICKETBOOKINGSTEP4, diff --git a/pubspec.yaml b/pubspec.yaml index f8ceb5c..3e867b8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,7 +47,7 @@ dependencies: firebase_core: ^3.11.0 cloud_firestore: ^5.6.4 firebase_auth: ^5.5.1 - shared_preferences: ^2.5.2 + shared_preferences: ^2.4.6 logger: ^2.5.0 dropdown_button2: ^2.3.9 google_fonts: ^6.2.1