Feat: done post data for add_passenger_screen

This commit is contained in:
orangdeso 2025-03-13 12:10:14 +07:00
parent 430cce5f10
commit e447c93584
25 changed files with 417 additions and 63 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
#Wed Mar 12 19:14:57 WIB 2025
#Thu Mar 13 10:11:24 WIB 2025
base.0=D\:\\Flutter\\Flutter Project\\e_porter\\build\\app\\intermediates\\dex\\debug\\mergeExtDexDebug\\classes.dex
base.1=D\:\\Flutter\\Flutter Project\\e_porter\\build\\app\\intermediates\\dex\\debug\\mergeLibDexDebug\\0\\classes.dex
base.2=D\:\\Flutter\\Flutter Project\\e_porter\\build\\app\\intermediates\\dex\\debug\\mergeProjectDexDebug\\0\\classes.dex

View File

@ -4,12 +4,14 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class DropdownComponent extends StatefulWidget {
final List<String> items;
final String? value;
final Function(String?) onChanged;
final String hintText;
const DropdownComponent({
Key? key,
required this.items,
this.value,
required this.onChanged,
required this.hintText,
@ -20,9 +22,7 @@ class DropdownComponent extends StatefulWidget {
}
class _DropdownComponentState extends State<DropdownComponent> {
final List<String> items = ["KTP", "Paspor"];
String? selectedValue;
bool _isMenuOpen = false;
@override
@ -39,7 +39,7 @@ class _DropdownComponentState extends State<DropdownComponent> {
child: DropdownButton2<String>(
isExpanded: true,
value: selectedValue,
items: items.map((String item) {
items: widget.items.map((String item) {
return DropdownMenuItem<String>(
value: item,
child: Text(
@ -77,7 +77,9 @@ class _DropdownComponentState extends State<DropdownComponent> {
padding: EdgeInsets.only(left: 10.w, right: 16.w, top: 4.h, bottom: 4.h),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.r),
border: Border.all(width: 1.w, color: _isMenuOpen ? PrimaryColors.primary800 : GrayColors.gray200),
border: Border.all(
width: 1.w,
color: _isMenuOpen ? PrimaryColors.primary800 : GrayColors.gray200),
color: Colors.white,
),
),

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../constants/colors.dart';
@ -6,15 +7,27 @@ import '../../../constants/colors.dart';
class TextFieldComponent extends StatelessWidget {
final TextEditingController? controller;
final String hintText;
final String? Function(String?)? validators;
final TextInputType? textInputType;
final List<TextInputFormatter>? inputFormatters;
const TextFieldComponent({this.controller, required this.hintText});
const TextFieldComponent({
this.controller,
required this.hintText,
this.validators,
this.textInputType,
this.inputFormatters
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10.r)),
child: TextField(
child: TextFormField(
controller: controller,
validator: validators,
keyboardType: textInputType,
inputFormatters: inputFormatters,
decoration: InputDecoration(
prefix: Padding(padding: EdgeInsets.symmetric(horizontal: 10.w)),
hintText: hintText,

View File

@ -27,11 +27,21 @@ class GrayColors {
}
class RedColors {
static const red100 = Color(0xFFFDE8E8);
static const red200 = Color(0xFFFBD5D5);
static const red300 = Color(0xFFF8B4B4);
static const red400 = Color(0xFFF98080);
static const red500 = Color(0xFFF05252);
static const red600 = Color(0xFFE02424);
}
class GreenColors {
static const green50 = Color(0xFFE8F6F0);
static const green100 = Color(0xFFDEF7EC);
static const green200 = Color(0xFFBCF0DA);
static const green300 = Color(0xFF84E1BC);
static const green400 = Color(0xFF31C48D);
static const green500 = Color(0xFF0E9F6E);
static const green600 = Color(0xFF057A55);
}

View File

@ -0,0 +1,11 @@
import 'package:cloud_firestore/cloud_firestore.dart';
Future<bool> isNoIdUnique(String userId, String noId) async {
final querySnapshot = await FirebaseFirestore.instance
.collection('users')
.doc(userId)
.collection('passenger')
.where('noId', isEqualTo: noId)
.get();
return querySnapshot.docs.isEmpty;
}

View File

@ -0,0 +1,11 @@
import 'package:flutter/services.dart';
// Formatter untuk mengubah input menjadi uppercase
class UpperCaseTextFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
return newValue.copyWith(
text: newValue.text.toUpperCase(),
);
}
}

View File

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../constants/colors.dart';
class SnackbarHelper {
static void showSuccess(String title, String message) {
Get.snackbar(
title,
message,
backgroundColor: GreenColors.green500,
colorText: Colors.white,
);
}
static void showError(String title, String message) {
Get.snackbar(
title,
message,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
static void showInfo(String title, String message) {
Get.snackbar(
title,
message,
);
}
}

View File

@ -12,4 +12,22 @@ class Validators {
}
return null;
}
static String? validatorName(String? value) {
if (value == null || value.isEmpty) {
return 'Nama Lengkap tidak boleh kosong';
}
return null;
}
static String? validatorNoID(String? value) {
if (value == null || value.isEmpty) {
return 'No ID tidak boleh kosong';
} else if (value.length > 16) {
return 'No ID tidak boleh lebih dari 16 digit';
} else if (value.length < 16) {
return 'No ID kurang dari 16 digit';
}
return null;
}
}

View File

@ -67,7 +67,9 @@ class AuthRepositoryImpl implements AuthRepository {
if (docSnapshot.exists) {
final data = docSnapshot.data();
if (data != null) {
return UserData.fromMap(data);
final userData = UserData.fromMap(data);
final updatedUserData = userData.copyWith(uid: docSnapshot.id);
return updatedUserData;
}
}
return null;

View File

@ -0,0 +1,25 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:e_porter/domain/repositories/profil_repository.dart';
import '../../_core/service/logger_service.dart';
import '../../domain/models/user_entity.dart';
class ProfilRepositoryImpl implements ProfilRepository {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
@override
Future<void> createPassenger({
required String userId,
required PassengerModel passenger,
}) async {
try {
DocumentReference docRef = await _firestore
.collection('users')
.doc(userId)
.collection('passenger')
.add(passenger.toMap());
logger.d("Passenger doc id: ${docRef.id}");
} catch (e) {
rethrow;
}
}
}

View File

@ -0,0 +1,20 @@
import 'package:get/get.dart';
import '../../data/repositories/profil_repository_impl.dart';
import '../../presentation/controllers/profil_controller.dart';
import '../repositories/profil_repository.dart';
import '../usecases/profil_usecase.dart';
class ProfilBinding extends Bindings {
@override
void dependencies() {
// Injeksi repository
Get.lazyPut<ProfilRepository>(() => ProfilRepositoryImpl());
// Injeksi use case, menggunakan repository yang telah di-inject
Get.lazyPut(() => CreatePassengerUseCase(Get.find()));
// Injeksi controller, menggunakan use case yang sudah tersedia
Get.lazyPut(() => ProfilController(createPassengerUseCase: Get.find()));
}
}

View File

@ -11,6 +11,7 @@ class UserEntity {
}
class UserData {
final String uid;
final String? tipeId;
final String? noId;
final String? name;
@ -24,6 +25,7 @@ class UserData {
final String? role;
UserData({
required this.uid,
required this.tipeId,
required this.noId,
required this.name,
@ -50,6 +52,7 @@ class UserData {
}
return UserData(
uid: map['uid'] ?? '',
tipeId: map['tipeId'] ?? '',
noId: map['noId'] ?? '',
name: map['name'] as String?,
@ -66,6 +69,7 @@ class UserData {
Map<String, dynamic> toMap() {
return {
'uid': uid,
'tipeId': tipeId,
'noId': noId,
'name': name,
@ -79,4 +83,66 @@ class UserData {
'role': role,
};
}
UserData copyWith({
String? uid,
String? tipeId,
String? noId,
String? name,
String? email,
String? phone,
String? birthDate,
String? gender,
String? work,
String? city,
String? address,
String? role,
}) {
return UserData(
uid: uid ?? this.uid,
tipeId: tipeId ?? this.tipeId,
noId: noId ?? this.noId,
name: name ?? this.name,
email: email ?? this.email,
phone: phone ?? this.phone,
birthDate: birthDate ?? this.birthDate,
gender: gender ?? this.gender,
work: work ?? this.work,
city: city ?? this.city,
address: address ?? this.address,
role: role ?? this.role,
);
}
}
class PassengerModel {
final String typeId;
final String noId;
final String name;
final String gender;
PassengerModel({
required this.typeId,
required this.noId,
required this.name,
required this.gender,
});
Map<String, dynamic> toMap() {
return {
'typeId': typeId,
'noId': noId,
'name': name,
'gender': gender,
};
}
factory PassengerModel.fromMap(Map<String, dynamic> map) {
return PassengerModel(
typeId: map['typeId'] ?? '',
noId: map['noId'] ?? '',
name: map['name'] ?? '',
gender: map['gender'] ?? '',
);
}
}

View File

@ -0,0 +1,8 @@
import '../models/user_entity.dart';
abstract class ProfilRepository {
Future<void> createPassenger({
required String userId,
required PassengerModel passenger,
});
}

View File

@ -0,0 +1,19 @@
import '../models/user_entity.dart';
import '../repositories/profil_repository.dart';
class CreatePassengerUseCase {
final ProfilRepository profilRepository;
CreatePassengerUseCase(this.profilRepository);
Future<void> call({
required String userId,
required PassengerModel passenger,
}) async {
// Lakukan validasi atau logika bisnis lain jika diperlukan
await profilRepository.createPassenger(
userId: userId,
passenger: passenger,
);
}
}

View File

@ -41,7 +41,7 @@ class AuthController extends GetxController {
final uid = userEntity.uid;
final roleFromDB = await getUserRoleUseCase(uid);
logger.d("roleFromDB: $roleFromDB, roleFromOnboarding: $roleFromOnboarding");
logger.d("roleFromDB: $roleFromDB, roleFromOnboarding: $roleFromOnboarding, UID: $uid");
if (roleFromDB != null && roleFromOnboarding != null && roleFromDB != roleFromOnboarding) {
_showErrorSnackbar(
@ -58,14 +58,13 @@ class AuthController extends GetxController {
}
if (userData.role!.toLowerCase() != effectiveRole.toLowerCase()) {
_showErrorSnackbar("Role Tidak Sesuai",
"Data user menunjukkan role '${userData.role}', bukan '$effectiveRole'.");
_showErrorSnackbar(
"Role Tidak Sesuai", "Data user menunjukkan role '${userData.role}', bukan '$effectiveRole'.");
return;
}
await PreferencesService.saveUserData(userData);
Get.offAllNamed(Routes.NAVBAR, arguments: effectiveRole);
} on AuthException catch (e) {
_showErrorSnackbar("Login Gagal", e.message);
} catch (e) {

View File

@ -0,0 +1,29 @@
import 'package:get/get.dart';
import '../../domain/models/user_entity.dart';
import '../../domain/usecases/profil_usecase.dart';
class ProfilController extends GetxController {
final CreatePassengerUseCase createPassengerUseCase;
ProfilController({required this.createPassengerUseCase});
Future<void> addPassenger({
required String userId,
required String typeId,
required String noId,
required String name,
required String gender,
}) async {
final passenger = PassengerModel(
typeId: typeId,
noId: noId,
name: name,
gender: gender,
);
await createPassengerUseCase(
userId: userId,
passenger: passenger,
);
}
}

View File

@ -5,10 +5,19 @@ import 'package:e_porter/_core/component/text_field/dropdown/dropdown_component.
import 'package:e_porter/_core/component/text_field/text_input/text_field_component.dart';
import 'package:e_porter/_core/constants/colors.dart';
import 'package:e_porter/_core/constants/typography.dart';
import 'package:e_porter/_core/utils/snackbar/snackbar_helper.dart';
import 'package:e_porter/_core/validators/validators.dart';
import 'package:e_porter/presentation/controllers/profil_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../../../_core/service/logger_service.dart';
import '../../../../_core/service/preferences_service.dart';
import '../../../../_core/utils/firestore/unique_helper.dart';
import '../../../../_core/utils/formatter/uppercase_helper.dart';
class AddPassengerScreen extends StatefulWidget {
const AddPassengerScreen({super.key});
@ -17,7 +26,13 @@ class AddPassengerScreen extends StatefulWidget {
}
class _AddPassengerScreenState extends State<AddPassengerScreen> {
final ValueNotifier<String> selectedGender = ValueNotifier<String>('');
final ValueNotifier<String> selectedGender = ValueNotifier<String>('Laki-laki');
final TextEditingController _nameController = TextEditingController();
final TextEditingController _noIdController = TextEditingController();
final ProfilController profilController = Get.find<ProfilController>();
final _formKey = GlobalKey<FormState>();
String selectedTypeId = 'KTP';
@override
Widget build(BuildContext context) {
@ -34,59 +49,85 @@ class _AddPassengerScreenState extends State<AddPassengerScreen> {
body: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TypographyStyles.body(
"Silahkan isi informasi data diri calon penumpang baru",
color: GrayColors.gray600,
fontWeight: FontWeight.w400,
maxlines: 2,
),
SizedBox(height: 32.h),
TypographyStyles.body('Nama Lengkap', color: GrayColors.gray600, fontWeight: FontWeight.w400),
SizedBox(height: 16.w),
TextFieldComponent(hintText: 'Masukkan nama lengkap'),
SizedBox(height: 20.h),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TypographyStyles.body('Tipe ID', color: GrayColors.gray600, fontWeight: FontWeight.w400),
SizedBox(height: 16.h),
DropdownComponent(
hintText: "Pilih jenis dokument",
value: "KTP",
onChanged: (value) {},
),
],
),
SizedBox(width: 16.w),
Expanded(
child: Column(
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TypographyStyles.body(
"Silahkan isi informasi data diri calon penumpang baru",
color: GrayColors.gray600,
fontWeight: FontWeight.w400,
maxlines: 2,
),
SizedBox(height: 32.h),
TypographyStyles.body('Nama Lengkap', color: GrayColors.gray600, fontWeight: FontWeight.w400),
SizedBox(height: 16.w),
TextFieldComponent(
controller: _nameController,
hintText: 'Masukkan nama lengkap',
validators: Validators.validatorName,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\s]')),
UpperCaseTextFormatter(),
],
textInputType: TextInputType.text,
),
SizedBox(height: 20.h),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TypographyStyles.body('No ID', color: GrayColors.gray600, fontWeight: FontWeight.w400),
TypographyStyles.body('Tipe ID', color: GrayColors.gray600, fontWeight: FontWeight.w400),
SizedBox(height: 16.h),
TextFieldComponent(hintText: 'Masukkan ID')
DropdownComponent(
hintText: "Pilih jenis dokument",
items: ['KTP', 'Pasport'],
value: selectedTypeId,
onChanged: (value) {
setState(() {
selectedTypeId = value!;
});
},
),
],
),
)
],
),
SizedBox(height: 20.w),
TypographyStyles.body('Jenis Kelamin', color: GrayColors.gray600, fontWeight: FontWeight.w400),
Row(
children: [
_buildRadioButton(context, label: 'Laki-laki', value: 'Laki-laki'),
SizedBox(width: 40.h),
_buildRadioButton(context, label: 'Perempuan', value: 'Perempuan')
],
)
],
SizedBox(width: 16.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TypographyStyles.body('No ID', color: GrayColors.gray600, fontWeight: FontWeight.w400),
SizedBox(height: 16.h),
TextFieldComponent(
controller: _noIdController,
hintText: 'Masukkan ID',
validators: Validators.validatorNoID,
textInputType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(16),
],
)
],
),
)
],
),
SizedBox(height: 20.w),
TypographyStyles.body('Jenis Kelamin', color: GrayColors.gray600, fontWeight: FontWeight.w400),
Row(
children: [
_buildRadioButton(context, label: 'Laki-laki', value: 'Laki-laki'),
SizedBox(width: 40.h),
_buildRadioButton(context, label: 'Perempuan', value: 'Perempuan')
],
)
],
),
),
),
),
@ -97,7 +138,11 @@ class _AddPassengerScreenState extends State<AddPassengerScreen> {
child: ButtonFill(
text: 'Simpan',
textColor: Colors.white,
onTap: () {},
onTap: () async {
if (_formKey.currentState!.validate()) {
_onSavePassenger();
}
},
),
),
);
@ -128,4 +173,47 @@ class _AddPassengerScreenState extends State<AddPassengerScreen> {
},
);
}
Future<void> _onSavePassenger() async {
final name = _nameController.text.trim();
final noId = _noIdController.text.trim();
final gender = selectedGender.value;
final typeId = selectedTypeId;
if (name.isEmpty || noId.isEmpty) {
Get.snackbar('Error', 'Nama dan No ID tidak boleh kosong');
return;
}
try {
final userData = await PreferencesService.getUserData();
if (userData == null || userData.uid.isEmpty) {
SnackbarHelper.showInfo('Error', 'User ID tidak ditemukan, silakan login kembali');
return;
}
final userId = userData.uid;
final bool isUnique = await isNoIdUnique(userId, noId);
if (!isUnique) {
SnackbarHelper.showError('Error', 'No ID sudah digunakan. Harap gunakan No ID yang berbeda');
return;
}
await profilController.addPassenger(
userId: userId,
typeId: typeId,
noId: noId,
name: name,
gender: gender,
);
_nameController.clear();
_noIdController.clear();
SnackbarHelper.showSuccess('Sukses', 'Berhasil menambahkan penumpang baru');
logger.d("Berhasil menambah penumpang: {name: $name, typeId: $typeId, noId: $noId, gender: $gender}");
} catch (e) {
SnackbarHelper.showError('Error', 'Gagal menambahkan penumpang baru: $e');
logger.e("Gagal menambah penumpang: $e");
}
}
}

View File

@ -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/profil_binding.dart';
import 'package:e_porter/domain/bindings/search_flight_binding.dart';
import 'package:e_porter/domain/bindings/ticket_binding.dart';
import 'package:e_porter/presentation/screens/auth/pages/forget_password_screen.dart';
@ -116,6 +117,7 @@ class AppRoutes {
GetPage(
name: Routes.ADDPASSENGER,
page: () => AddPassengerScreen(),
binding: ProfilBinding(),
),
];
}