Feat: add scan qr and slicing design search porter after scan qr
This commit is contained in:
parent
0668f8d3dc
commit
b1f9d95907
|
@ -80,11 +80,6 @@ class TransactionRepositoryImpl implements TransactionRepository {
|
||||||
.doc(transactionId)
|
.doc(transactionId)
|
||||||
.set(transactionData);
|
.set(transactionData);
|
||||||
|
|
||||||
// Update ID booking pada tiket
|
|
||||||
await _firestore.collection('tickets').doc(ticketId).update({
|
|
||||||
'idBooking': idBooking,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Kelompokkan kursi berdasarkan kelas
|
// Kelompokkan kursi berdasarkan kelas
|
||||||
Map<String, List<int>> seatsByClass = {};
|
Map<String, List<int>> seatsByClass = {};
|
||||||
for (String seatNumber in numberSeat) {
|
for (String seatNumber in numberSeat) {
|
||||||
|
|
|
@ -1,22 +1,59 @@
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:e_porter/_core/component/button/button_outline.dart';
|
||||||
import 'package:e_porter/_core/component/dotted/dashed_line_component.dart';
|
import 'package:e_porter/_core/component/dotted/dashed_line_component.dart';
|
||||||
import 'package:e_porter/_core/constants/colors.dart';
|
import 'package:e_porter/_core/constants/colors.dart';
|
||||||
import 'package:e_porter/_core/constants/typography.dart';
|
import 'package:e_porter/_core/constants/typography.dart';
|
||||||
|
import 'package:e_porter/presentation/controllers/history_controller.dart';
|
||||||
import 'package:e_porter/presentation/screens/boarding_pass/component/card_details_passenger.dart';
|
import 'package:e_porter/presentation/screens/boarding_pass/component/card_details_passenger.dart';
|
||||||
import 'package:e_porter/presentation/screens/routes/app_rountes.dart';
|
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:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:zoom_tap_animation/zoom_tap_animation.dart';
|
import 'package:zoom_tap_animation/zoom_tap_animation.dart';
|
||||||
|
|
||||||
import '../../../../_core/component/appbar/appbar_component.dart';
|
import '../../../../_core/component/appbar/appbar_component.dart';
|
||||||
import '../../../../_core/component/button/button_fill.dart';
|
import '../../../../_core/component/button/button_fill.dart';
|
||||||
import '../../../../_core/component/card/custome_shadow_cotainner.dart';
|
import '../../../../_core/component/card/custome_shadow_cotainner.dart';
|
||||||
|
import '../../../../_core/utils/snackbar/snackbar_helper.dart';
|
||||||
|
import '../../../../domain/models/transaction_model.dart';
|
||||||
import '../../home/component/title_show_modal.dart';
|
import '../../home/component/title_show_modal.dart';
|
||||||
|
|
||||||
class DetailTicketScreen extends StatelessWidget {
|
class DetailTicketScreen extends StatefulWidget {
|
||||||
const DetailTicketScreen({super.key});
|
const DetailTicketScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DetailTicketScreen> createState() => _DetailTicketScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DetailTicketScreenState extends State<DetailTicketScreen> {
|
||||||
|
late final String id_transaction;
|
||||||
|
late final String id_ticket;
|
||||||
|
|
||||||
|
final historyController = Get.find<HistoryController>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final args = Get.arguments as Map<String, dynamic>;
|
||||||
|
id_transaction = args['id_transaction'];
|
||||||
|
id_ticket = args['id_ticket'];
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_loadTransactionData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadTransactionData() async {
|
||||||
|
try {
|
||||||
|
await historyController.getTransactionFromFirestore(id_ticket, id_transaction);
|
||||||
|
} catch (e) {
|
||||||
|
log('Error getTransaction $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
@ -30,145 +67,147 @@ class DetailTicketScreen extends StatelessWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Obx(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
|
() {
|
||||||
child: SingleChildScrollView(
|
if (historyController.isLoading.value) {
|
||||||
child: Column(
|
return Center(child: CircularProgressIndicator());
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
}
|
||||||
children: [
|
|
||||||
TypographyStyles.caption(
|
if (historyController.errorMessage.value.isNotEmpty) {
|
||||||
"Kode Booking Maskapai",
|
return Center(
|
||||||
color: GrayColors.gray500,
|
child: TypographyStyles.body(historyController.errorMessage.value, color: GrayColors.gray400),
|
||||||
fontWeight: FontWeight.w500,
|
);
|
||||||
),
|
}
|
||||||
SizedBox(height: 6.h),
|
|
||||||
TypographyStyles.body("I2L8JRL", color: GrayColors.gray800),
|
final transaction = historyController.selectedTransaction.value;
|
||||||
Padding(
|
if (transaction == null) {
|
||||||
padding: EdgeInsets.symmetric(vertical: 20.h),
|
return Center(child: TypographyStyles.body('Data transaksi tidak ditemukan', color: GrayColors.gray400));
|
||||||
child: CustomDashedLine(),
|
}
|
||||||
),
|
|
||||||
Row(
|
log('ID Booking Detail Tiket: ${transaction.idBooking}');
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
TypographyStyles.body("Citilink (103)", color: GrayColors.gray800),
|
TypographyStyles.caption(
|
||||||
SizedBox(width: 10.w),
|
"Kode Booking Maskapai",
|
||||||
SvgPicture.asset('assets/images/citilink.svg', width: 40.w, height: 10.h),
|
color: GrayColors.gray500,
|
||||||
],
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
SizedBox(height: 4.h),
|
SizedBox(height: 6.h),
|
||||||
Row(
|
TypographyStyles.body("${transaction.idBooking}", color: GrayColors.gray800),
|
||||||
children: [
|
Padding(
|
||||||
TypographyStyles.small("Fast Track (FT)", color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
padding: EdgeInsets.symmetric(vertical: 20.h),
|
||||||
SizedBox(width: 10.w),
|
child: CustomDashedLine(),
|
||||||
CircleAvatar(radius: 2.r, backgroundColor: Color(0xFFD9D9D9)),
|
),
|
||||||
SizedBox(width: 10.w),
|
Row(
|
||||||
TypographyStyles.small("Economy", color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 16.h),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
TypographyStyles.caption("12:20", color: GrayColors.gray800),
|
TypographyStyles.body(
|
||||||
TypographyStyles.small("Sen, 27 Jan", color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
"${transaction.flightDetails['airLines'] ?? 'Airline'} (${transaction.flightDetails['code'] ?? ''})",
|
||||||
SizedBox(height: 20.h),
|
color: GrayColors.gray800,
|
||||||
TypographyStyles.small("5j 40m", color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
),
|
||||||
SizedBox(height: 20.h),
|
SizedBox(width: 10.w),
|
||||||
TypographyStyles.caption("12:20", color: GrayColors.gray800),
|
SvgPicture.asset('assets/images/citilink.svg', width: 40.w, height: 10.h),
|
||||||
TypographyStyles.small("Sen, 27 Jan", color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(width: 20.w),
|
SizedBox(height: 4.h),
|
||||||
SvgPicture.asset('assets/images/garis.svg', height: 100.h),
|
Row(
|
||||||
SizedBox(width: 20.w),
|
children: [
|
||||||
Expanded(
|
TypographyStyles.small("Fast Track (FT)",
|
||||||
child: Column(
|
color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
SizedBox(width: 10.w),
|
||||||
children: [
|
CircleAvatar(radius: 2.r, backgroundColor: Color(0xFFD9D9D9)),
|
||||||
TypographyStyles.caption("Yogyakarta (YIA)", color: GrayColors.gray800),
|
SizedBox(width: 10.w),
|
||||||
TypographyStyles.caption(
|
TypographyStyles.small("${transaction.flightDetails['flightClass']}",
|
||||||
"Bandar YIA, Terminal Domestic",
|
color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
||||||
color: GrayColors.gray600,
|
],
|
||||||
fontWeight: FontWeight.w400,
|
),
|
||||||
maxlines: 2,
|
SizedBox(height: 16.h),
|
||||||
overflow: TextOverflow.ellipsis,
|
Row(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TypographyStyles.caption("12:20", color: GrayColors.gray800),
|
||||||
|
TypographyStyles.small("Sen, 27 Jan",
|
||||||
|
color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
||||||
|
SizedBox(height: 20.h),
|
||||||
|
TypographyStyles.small("5j 40m", color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
||||||
|
SizedBox(height: 20.h),
|
||||||
|
TypographyStyles.caption("12:20", color: GrayColors.gray800),
|
||||||
|
TypographyStyles.small("Sen, 27 Jan",
|
||||||
|
color: GrayColors.gray600, fontWeight: FontWeight.w400),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(width: 20.w),
|
||||||
|
SvgPicture.asset('assets/images/garis.svg', height: 100.h),
|
||||||
|
SizedBox(width: 20.w),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TypographyStyles.caption(
|
||||||
|
"${transaction.flightDetails['cityDeparture']} (${transaction.flightDetails['codeDeparture']})",
|
||||||
|
color: GrayColors.gray800,
|
||||||
|
),
|
||||||
|
TypographyStyles.caption(
|
||||||
|
"${transaction.bandaraDetails['departure']?['name']}",
|
||||||
|
color: GrayColors.gray600,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
maxlines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
SizedBox(height: 58.h),
|
||||||
|
TypographyStyles.caption(
|
||||||
|
"${transaction.flightDetails['cityArrival']} (${transaction.flightDetails['codeArrival']})",
|
||||||
|
color: GrayColors.gray800,
|
||||||
|
),
|
||||||
|
TypographyStyles.caption(
|
||||||
|
"${transaction.bandaraDetails['arrival']?['name']}",
|
||||||
|
color: GrayColors.gray600,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
maxlines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 58.h),
|
)
|
||||||
TypographyStyles.caption("Lombok (LOP)", color: GrayColors.gray800),
|
],
|
||||||
TypographyStyles.caption(
|
),
|
||||||
"Bandar Zainuddin Abdul Madjid, Terminal Domestic",
|
Padding(
|
||||||
color: GrayColors.gray600,
|
padding: EdgeInsets.symmetric(vertical: 20.h),
|
||||||
fontWeight: FontWeight.w400,
|
child: CustomDashedLine(),
|
||||||
maxlines: 2,
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
TypographyStyles.h6("Detail Penumpang", color: GrayColors.gray800),
|
||||||
)
|
SizedBox(height: 20.h),
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 20.h),
|
|
||||||
child: CustomDashedLine(),
|
|
||||||
),
|
|
||||||
TypographyStyles.h6("Detail Penumpang", color: GrayColors.gray800),
|
|
||||||
SizedBox(height: 20.h),
|
|
||||||
ListView.builder(
|
|
||||||
itemCount: 1,
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: NeverScrollableScrollPhysics(),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return CardDetailsPassenger(
|
|
||||||
name: 'AHMAD CHOIRUL UMAM ALI',
|
|
||||||
typeId: 'KTP',
|
|
||||||
noId: '3571••••••••••03',
|
|
||||||
seatClass: 'Economy',
|
|
||||||
numberSeat: '10 F',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
bottomNavigationBar: CustomeShadowCotainner(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
|
|
||||||
child: ButtonFill(
|
|
||||||
text: 'Cetak Boarding Pass',
|
|
||||||
textColor: Colors.white,
|
|
||||||
onTap: () {
|
|
||||||
Get.bottomSheet(
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
isScrollControlled: true,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(10.r),
|
|
||||||
topRight: Radius.circular(10.r),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(left: 16.w, right: 16.w, bottom: 16.h),
|
|
||||||
child: Wrap(
|
|
||||||
children: [
|
|
||||||
TitleShowModal(text: 'Cetak Boarding Pass'),
|
|
||||||
SizedBox(height: 30.h),
|
|
||||||
ListView.builder(
|
ListView.builder(
|
||||||
itemCount: 1,
|
itemCount: transaction.passengerDetails.length,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
|
physics: NeverScrollableScrollPhysics(),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return Padding(
|
final passenger = transaction.passengerDetails[index];
|
||||||
padding: EdgeInsets.only(top: 16.h),
|
final seatNumber =
|
||||||
child: _buildCetakBoardingPass(
|
index < transaction.numberSeat.length ? transaction.numberSeat[index] : 'N/A';
|
||||||
context,
|
|
||||||
name: 'AHMAD CHOIRUL UMAM',
|
String maskedId = 'N/A';
|
||||||
typeId: 'KTP',
|
if (passenger['noId'] != null) {
|
||||||
noId: '3571••••••••••03',
|
String idNumber = passenger['noId'].toString();
|
||||||
onTap: () {
|
if (idNumber.length > 4) {
|
||||||
Get.toNamed(Routes.PRINTBOARDINGPASS);
|
maskedId = '${idNumber.substring(0, 4)}${'•' * (idNumber.length - 4)}';
|
||||||
},
|
} else {
|
||||||
),
|
maskedId = idNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CardDetailsPassenger(
|
||||||
|
name: passenger['name'] ?? 'N/A',
|
||||||
|
typeId: passenger['typeId'] ?? 'N/A',
|
||||||
|
noId: maskedId,
|
||||||
|
seatClass: '${transaction.flightDetails['flightClass']}',
|
||||||
|
numberSeat: seatNumber,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -179,6 +218,151 @@ class DetailTicketScreen extends StatelessWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
bottomNavigationBar: Obx(() {
|
||||||
|
if (historyController.isLoading.value) {
|
||||||
|
return SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final transaction = historyController.selectedTransaction.value;
|
||||||
|
if (transaction == null) {
|
||||||
|
return SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transaction.status == 'pending') {
|
||||||
|
return CustomeShadowCotainner(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
|
||||||
|
child: ButtonFill(
|
||||||
|
text: 'Upload Bukti Pembayaran',
|
||||||
|
textColor: Colors.white,
|
||||||
|
onTap: () {
|
||||||
|
Get.toNamed(Routes.UPLOADFILE, arguments: {
|
||||||
|
'ticketId': transaction.ticketId,
|
||||||
|
'transactionId': transaction.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (transaction.status == 'active') {
|
||||||
|
if (transaction.porterServiceDetails == null ||
|
||||||
|
(transaction.porterServiceDetails as Map<String, dynamic>).isEmpty) {
|
||||||
|
return CustomeShadowCotainner(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
|
||||||
|
child: ButtonFill(
|
||||||
|
text: 'Cetak Boarding Pass',
|
||||||
|
textColor: Colors.white,
|
||||||
|
onTap: () => _showCetakBoardingPassBottomSheet(transaction),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return CustomeShadowCotainner(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ButtonOutline(
|
||||||
|
text: 'Scan QR Code Porter',
|
||||||
|
textColor: PrimaryColors.primary800,
|
||||||
|
onTap: () async {
|
||||||
|
await Permission.camera.status;
|
||||||
|
|
||||||
|
// final argument = {
|
||||||
|
// 'ticketId': transaction.ticketId,
|
||||||
|
// 'transactionId': transaction.id,
|
||||||
|
// };
|
||||||
|
// Get.toNamed(Routes.SCANQR, arguments: argument);
|
||||||
|
|
||||||
|
final result = await Get.toNamed(
|
||||||
|
Routes.SCANQR,
|
||||||
|
arguments: {
|
||||||
|
'ticketId': transaction.ticketId,
|
||||||
|
'transactionId': transaction.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Jika porter sibuk, tampilkan Snackbar dan hentikan di sini
|
||||||
|
if (result == 'PORTER_BUSY') {
|
||||||
|
SnackbarHelper.showError(
|
||||||
|
'Porter Tidak Tersedia',
|
||||||
|
'Tidak ada porter yang tersedia atau semua porter sedang sibuk, coba nanti.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: 12.h),
|
||||||
|
ButtonFill(
|
||||||
|
text: 'Cetak Boarding Pass',
|
||||||
|
textColor: Colors.white,
|
||||||
|
onTap: () => _showCetakBoardingPassBottomSheet(transaction),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return CustomeShadowCotainner(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
|
||||||
|
child: ButtonFill(
|
||||||
|
text: 'Cetak Boarding Pass',
|
||||||
|
textColor: Colors.white,
|
||||||
|
onTap: () => _showCetakBoardingPassBottomSheet(transaction),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showCetakBoardingPassBottomSheet(TransactionModel transaction) {
|
||||||
|
Get.bottomSheet(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
isScrollControlled: true,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(10.r),
|
||||||
|
topRight: Radius.circular(10.r),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(left: 16.w, right: 16.w, bottom: 16.h),
|
||||||
|
child: Wrap(
|
||||||
|
children: [
|
||||||
|
TitleShowModal(text: 'Cetak Boarding Pass'),
|
||||||
|
SizedBox(height: 30.h),
|
||||||
|
ListView.builder(
|
||||||
|
itemCount: transaction.passengerDetails.length,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final passenger = transaction.passengerDetails[index];
|
||||||
|
|
||||||
|
String maskedId = 'N/A';
|
||||||
|
if (passenger['noId'] != null) {
|
||||||
|
String idNumber = passenger['noId'].toString();
|
||||||
|
if (idNumber.length > 4) {
|
||||||
|
maskedId = '${idNumber.substring(0, 4)}${'•' * (idNumber.length - 4)}';
|
||||||
|
} else {
|
||||||
|
maskedId = idNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(top: 16.h),
|
||||||
|
child: _buildCetakBoardingPass(
|
||||||
|
context,
|
||||||
|
name: passenger['name'] ?? 'N/A',
|
||||||
|
typeId: passenger['typeId'] ?? '',
|
||||||
|
noId: maskedId,
|
||||||
|
onTap: () {
|
||||||
|
Get.toNamed(Routes.PRINTBOARDINGPASS,
|
||||||
|
arguments: {'transaction': transaction, 'passengerIndex': index});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,337 @@
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
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:mobile_scanner/mobile_scanner.dart';
|
||||||
|
|
||||||
|
import '../../../../_core/constants/colors.dart';
|
||||||
|
import '../../../../_core/constants/typography.dart';
|
||||||
|
import '../../../../_core/utils/snackbar/snackbar_helper.dart';
|
||||||
|
import '../../../../presentation/controllers/porter_queue_controller.dart';
|
||||||
|
import '../../../../presentation/controllers/transaction_controller.dart';
|
||||||
|
|
||||||
|
class ScanQRScreen extends StatefulWidget {
|
||||||
|
const ScanQRScreen({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ScanQRScreen> createState() => _ScanQRScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ScanQRScreenState extends State<ScanQRScreen> {
|
||||||
|
final MobileScannerController cameraController = MobileScannerController(
|
||||||
|
formats: [BarcodeFormat.qrCode, BarcodeFormat.aztec, BarcodeFormat.dataMatrix],
|
||||||
|
detectionSpeed: DetectionSpeed.unrestricted,
|
||||||
|
detectionTimeoutMs: 1000,
|
||||||
|
facing: CameraFacing.back,
|
||||||
|
torchEnabled: false,
|
||||||
|
returnImage: true,
|
||||||
|
);
|
||||||
|
bool isProcessing = false;
|
||||||
|
bool _isTorchOn = false;
|
||||||
|
|
||||||
|
String ticketId = '';
|
||||||
|
String transactionId = '';
|
||||||
|
|
||||||
|
final PorterQueueController _porterQueueController = Get.find<PorterQueueController>();
|
||||||
|
final TransactionController _transactionController = Get.find<TransactionController>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final args = Get.arguments as Map<String, dynamic>;
|
||||||
|
ticketId = args['ticketId'] ?? '';
|
||||||
|
transactionId = args['transactionId'] ?? '';
|
||||||
|
|
||||||
|
log('Transaction ID: $transactionId');
|
||||||
|
log('Current Transaction: ${_transactionController.currentTransaction.value}');
|
||||||
|
log('User Details: ${_transactionController.currentTransaction.value?.userDetails}');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
cameraController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetScanner() {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => isProcessing = false);
|
||||||
|
cameraController.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Scan QR Code Porter'),
|
||||||
|
backgroundColor: PrimaryColors.primary800,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
color: Colors.white,
|
||||||
|
icon: Icon(
|
||||||
|
_isTorchOn ? Icons.flash_on : Icons.flash_off,
|
||||||
|
color: _isTorchOn ? Colors.yellow : Colors.white,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
cameraController.toggleTorch();
|
||||||
|
setState(() {
|
||||||
|
_isTorchOn = !_isTorchOn;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
MobileScanner(
|
||||||
|
// controller: cameraController,
|
||||||
|
scanWindow: Rect.fromCenter(
|
||||||
|
center: Offset(
|
||||||
|
MediaQuery.of(context).size.width / 2,
|
||||||
|
MediaQuery.of(context).size.height / 2,
|
||||||
|
),
|
||||||
|
width: MediaQuery.of(context).size.width * 0.7,
|
||||||
|
height: MediaQuery.of(context).size.width * 0.7,
|
||||||
|
),
|
||||||
|
onDetect: (capture) {
|
||||||
|
final raw = capture.barcodes.isNotEmpty ? capture.barcodes[0].rawValue : null;
|
||||||
|
if (raw != null && !isProcessing) {
|
||||||
|
_processQRCode(raw);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ClipPath(
|
||||||
|
clipper: OverlayClipper(),
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.7,
|
||||||
|
height: MediaQuery.of(context).size.width * 0.7,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: Colors.white, width: 3),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.6,
|
||||||
|
height: MediaQuery.of(context).size.width * 0.6,
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: CornerPainter(
|
||||||
|
cornerColor: Colors.green,
|
||||||
|
cornerSize: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 40,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: 16.w),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.9),
|
||||||
|
borderRadius: BorderRadius.circular(10.r),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TypographyStyles.body(
|
||||||
|
'Scan QR code di lokasi porter',
|
||||||
|
color: GrayColors.gray800,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
SizedBox(height: 4.h),
|
||||||
|
TypographyStyles.caption(
|
||||||
|
'Posisikan kamera ke QR code yang tersedia di lokasi porter',
|
||||||
|
color: GrayColors.gray600,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isProcessing)
|
||||||
|
Container(
|
||||||
|
color: Colors.black.withOpacity(0.6),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(color: Colors.white),
|
||||||
|
SizedBox(height: 16.h),
|
||||||
|
TypographyStyles.body(
|
||||||
|
'Sedang mencari porter…',
|
||||||
|
color: Colors.white,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _processQRCode(String rawLocation) async {
|
||||||
|
log('[ScanQRScreen] Starting QR process (loc: $rawLocation)');
|
||||||
|
|
||||||
|
setState(() => isProcessing = true);
|
||||||
|
await cameraController.stop();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Load transaksi jika perlu
|
||||||
|
if (_transactionController.currentTransaction.value == null) {
|
||||||
|
await _transactionController.getTransactionById(
|
||||||
|
ticketId: ticketId,
|
||||||
|
transactionId: transactionId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Ambil passengerId
|
||||||
|
final userDetails = _transactionController.currentTransaction.value?.userDetails;
|
||||||
|
final passengerId = userDetails?['uid'] as String?;
|
||||||
|
if (passengerId == null || passengerId.isEmpty) {
|
||||||
|
SnackbarHelper.showError(
|
||||||
|
'Error',
|
||||||
|
'User tidak dikenali, silakan login ulang atau coba lagi.',
|
||||||
|
);
|
||||||
|
// Reset scanner agar bisa scan lagi
|
||||||
|
setState(() => isProcessing = false);
|
||||||
|
await cameraController.start();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Request porter
|
||||||
|
final result = await _porterQueueController.requestPorter(
|
||||||
|
passengerId: passengerId,
|
||||||
|
ticketId: ticketId,
|
||||||
|
transactionId: transactionId,
|
||||||
|
location: rawLocation,
|
||||||
|
);
|
||||||
|
await Future.delayed(Duration(seconds: 3));
|
||||||
|
|
||||||
|
log('[ScanQRScreen] requestPorter succeeded: $result');
|
||||||
|
|
||||||
|
SnackbarHelper.showSuccess(
|
||||||
|
'Porter Ditemukan!',
|
||||||
|
'Anda berhasil mendapatkan porter',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Sukses: navigasi ke Processing
|
||||||
|
Get.toNamed(
|
||||||
|
Routes.PROCESSING,
|
||||||
|
arguments: {
|
||||||
|
'location': rawLocation,
|
||||||
|
'ticketId': ticketId,
|
||||||
|
'transactionId': transactionId,
|
||||||
|
'porterId': result['porterId']!,
|
||||||
|
'porterTransactionId': result['transactionId']!,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} on Exception catch (e) {
|
||||||
|
final msg = e.toString();
|
||||||
|
if (msg.contains('Porter tidak tersedia')) {
|
||||||
|
// Porter sibuk → langsung pop dengan result
|
||||||
|
Get.back(result: 'PORTER_BUSY');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error umum lain
|
||||||
|
log('[ScanQRScreen] _processQRCode error: $e');
|
||||||
|
SnackbarHelper.showError('Error', 'Terjadi kesalahan: $e');
|
||||||
|
|
||||||
|
// Reset scanner agar bisa coba lagi
|
||||||
|
setState(() => isProcessing = false);
|
||||||
|
await cameraController.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OverlayClipper extends CustomClipper<Path> {
|
||||||
|
@override
|
||||||
|
Path getClip(Size size) {
|
||||||
|
final scanAreaSize = size.width * 0.7;
|
||||||
|
final rect = Rect.fromCenter(
|
||||||
|
center: Offset(size.width / 2, size.height / 2),
|
||||||
|
width: scanAreaSize,
|
||||||
|
height: scanAreaSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Path()
|
||||||
|
..addRect(Rect.fromLTWH(0, 0, size.width, size.height))
|
||||||
|
..addRRect(RRect.fromRectAndRadius(rect, Radius.circular(10)))
|
||||||
|
..fillType = PathFillType.evenOdd;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CornerPainter extends CustomPainter {
|
||||||
|
final Color cornerColor;
|
||||||
|
final double cornerSize;
|
||||||
|
|
||||||
|
CornerPainter({
|
||||||
|
required this.cornerColor,
|
||||||
|
required this.cornerSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()
|
||||||
|
..color = cornerColor
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 5;
|
||||||
|
|
||||||
|
// Top left
|
||||||
|
canvas.drawPath(
|
||||||
|
Path()
|
||||||
|
..moveTo(0, cornerSize)
|
||||||
|
..lineTo(0, 0)
|
||||||
|
..lineTo(cornerSize, 0),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Top right
|
||||||
|
canvas.drawPath(
|
||||||
|
Path()
|
||||||
|
..moveTo(size.width - cornerSize, 0)
|
||||||
|
..lineTo(size.width, 0)
|
||||||
|
..lineTo(size.width, cornerSize),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bottom right
|
||||||
|
canvas.drawPath(
|
||||||
|
Path()
|
||||||
|
..moveTo(size.width, size.height - cornerSize)
|
||||||
|
..lineTo(size.width, size.height)
|
||||||
|
..lineTo(size.width - cornerSize, size.height),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bottom left
|
||||||
|
canvas.drawPath(
|
||||||
|
Path()
|
||||||
|
..moveTo(cornerSize, size.height)
|
||||||
|
..lineTo(0, size.height)
|
||||||
|
..lineTo(0, size.height - cornerSize),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
import 'package:e_porter/_core/component/button/button_fill.dart';
|
||||||
|
import 'package:e_porter/_core/component/card/custome_shadow_cotainner.dart';
|
||||||
|
import 'package:e_porter/_core/constants/typography.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../../../../_core/component/appbar/appbar_component.dart';
|
||||||
|
import '../../../../_core/constants/colors.dart';
|
||||||
|
|
||||||
|
class ProcessingPorterScreen extends StatefulWidget {
|
||||||
|
const ProcessingPorterScreen({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ProcessingPorterScreen> createState() => _ProcessingPorterScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProcessingPorterScreenState extends State<ProcessingPorterScreen> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// final args = Get.arguments as Map<String, dynamic>;
|
||||||
|
// final location = args['location'] ?? '';
|
||||||
|
// final ticketId = args['ticketId'] ?? '';
|
||||||
|
// final transactionId = args['transactionId'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: GrayColors.gray50,
|
||||||
|
appBar: DefaultAppbarComponent(
|
||||||
|
title: 'Mencari Porter',
|
||||||
|
textColor: Colors.white,
|
||||||
|
backgroundColors: PrimaryColors.primary800,
|
||||||
|
onTab: () {
|
||||||
|
Get.back();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TypographyStyles.h1('Ilustrasi'),
|
||||||
|
SizedBox(height: 32.h),
|
||||||
|
CustomeShadowCotainner(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TypographyStyles.h6('Tunggu Portermu', color: GrayColors.gray800),
|
||||||
|
SizedBox(height: 4.h),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
TypographyStyles.caption(
|
||||||
|
'Tunngu dari pihak Porter merespon',
|
||||||
|
color: GrayColors.gray500,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
SizedBox(width: 4.w),
|
||||||
|
Icon(Icons.timelapse_outlined)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20.h),
|
||||||
|
CustomeShadowCotainner(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TypographyStyles.body('Ahmad Choirul Umam', color: GrayColors.gray800),
|
||||||
|
SizedBox(height: 10.h),
|
||||||
|
Divider(thickness: 1, color: GrayColors.gray200),
|
||||||
|
SizedBox(height: 10.h),
|
||||||
|
TypographyStyles.body('Lokasi', color: GrayColors.gray800),
|
||||||
|
SizedBox(height: 10.h),
|
||||||
|
_buildRowLocation(location: 'Gate Penerbangan', desc: 'Lokasi Anda'),
|
||||||
|
SizedBox(height: 10.h),
|
||||||
|
_buildRowLocation(location: 'Guyangan', desc: 'Lokasi Porter Anda'),
|
||||||
|
SizedBox(height: 10.h),
|
||||||
|
_buildRowLocation(location: 'Porter menuju ke lokasi anda', desc: 'Porter bergerak'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomNavigationBar: CustomeShadowCotainner(
|
||||||
|
child: ButtonFill(
|
||||||
|
text: 'Kembali ke menu',
|
||||||
|
textColor: Colors.white,
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRowLocation({required String location, required String desc}) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TypographyStyles.caption(
|
||||||
|
'10/10/2025',
|
||||||
|
color: GrayColors.gray600,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
TypographyStyles.caption(
|
||||||
|
'11:11',
|
||||||
|
color: GrayColors.gray600,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(width: 20.w),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TypographyStyles.caption(
|
||||||
|
location,
|
||||||
|
color: GrayColors.gray800,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
TypographyStyles.small(
|
||||||
|
desc,
|
||||||
|
color: GrayColors.gray600,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue