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.0=D\:\\Flutter\\Flutter Project\\e_porter\\build\\app\\intermediates\\dex\\debug\\mergeExtDexDebug\\classes.dex
base.1=D\:\\Flutter\\Flutter Project\\e_porter\\build\\app\\intermediates\\dex\\debug\\mergeLibDexDebug\\0\\classes.dex base.1=D\:\\Flutter\\Flutter Project\\e_porter\\build\\app\\intermediates\\dex\\debug\\mergeLibDexDebug\\0\\classes.dex
base.2=D\:\\Flutter\\Flutter Project\\e_porter\\build\\app\\intermediates\\dex\\debug\\mergeProjectDexDebug\\0\\classes.dex base.2=D\:\\Flutter\\Flutter Project\\e_porter\\build\\app\\intermediates\\dex\\debug\\mergeProjectDexDebug\\0\\classes.dex

View File

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

View File

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

View File

@ -27,11 +27,21 @@ class GrayColors {
} }
class RedColors { class RedColors {
static const red100 = Color(0xFFFDE8E8);
static const red200 = Color(0xFFFBD5D5); 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); static const red600 = Color(0xFFE02424);
} }
class GreenColors { class GreenColors {
static const green50 = Color(0xFFE8F6F0);
static const green100 = Color(0xFFDEF7EC); 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 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; 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) { if (docSnapshot.exists) {
final data = docSnapshot.data(); final data = docSnapshot.data();
if (data != null) { if (data != null) {
return UserData.fromMap(data); final userData = UserData.fromMap(data);
final updatedUserData = userData.copyWith(uid: docSnapshot.id);
return updatedUserData;
} }
} }
return null; 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 { class UserData {
final String uid;
final String? tipeId; final String? tipeId;
final String? noId; final String? noId;
final String? name; final String? name;
@ -24,6 +25,7 @@ class UserData {
final String? role; final String? role;
UserData({ UserData({
required this.uid,
required this.tipeId, required this.tipeId,
required this.noId, required this.noId,
required this.name, required this.name,
@ -50,6 +52,7 @@ class UserData {
} }
return UserData( return UserData(
uid: map['uid'] ?? '',
tipeId: map['tipeId'] ?? '', tipeId: map['tipeId'] ?? '',
noId: map['noId'] ?? '', noId: map['noId'] ?? '',
name: map['name'] as String?, name: map['name'] as String?,
@ -66,6 +69,7 @@ class UserData {
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'uid': uid,
'tipeId': tipeId, 'tipeId': tipeId,
'noId': noId, 'noId': noId,
'name': name, 'name': name,
@ -79,4 +83,66 @@ class UserData {
'role': role, '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 uid = userEntity.uid;
final roleFromDB = await getUserRoleUseCase(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) { if (roleFromDB != null && roleFromOnboarding != null && roleFromDB != roleFromOnboarding) {
_showErrorSnackbar( _showErrorSnackbar(
@ -58,14 +58,13 @@ class AuthController extends GetxController {
} }
if (userData.role!.toLowerCase() != effectiveRole.toLowerCase()) { if (userData.role!.toLowerCase() != effectiveRole.toLowerCase()) {
_showErrorSnackbar("Role Tidak Sesuai", _showErrorSnackbar(
"Data user menunjukkan role '${userData.role}', bukan '$effectiveRole'."); "Role Tidak Sesuai", "Data user menunjukkan role '${userData.role}', bukan '$effectiveRole'.");
return; return;
} }
await PreferencesService.saveUserData(userData); await PreferencesService.saveUserData(userData);
Get.offAllNamed(Routes.NAVBAR, arguments: effectiveRole); Get.offAllNamed(Routes.NAVBAR, arguments: effectiveRole);
} on AuthException catch (e) { } on AuthException catch (e) {
_showErrorSnackbar("Login Gagal", e.message); _showErrorSnackbar("Login Gagal", e.message);
} catch (e) { } 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/component/text_field/text_input/text_field_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/_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/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../../_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 { class AddPassengerScreen extends StatefulWidget {
const AddPassengerScreen({super.key}); const AddPassengerScreen({super.key});
@ -17,7 +26,13 @@ class AddPassengerScreen extends StatefulWidget {
} }
class _AddPassengerScreenState extends State<AddPassengerScreen> { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -34,59 +49,85 @@ class _AddPassengerScreenState extends State<AddPassengerScreen> {
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
child: SingleChildScrollView( child: Form(
child: Column( key: _formKey,
crossAxisAlignment: CrossAxisAlignment.start, child: SingleChildScrollView(
children: [ child: Column(
TypographyStyles.body( crossAxisAlignment: CrossAxisAlignment.start,
"Silahkan isi informasi data diri calon penumpang baru", children: [
color: GrayColors.gray600, TypographyStyles.body(
fontWeight: FontWeight.w400, "Silahkan isi informasi data diri calon penumpang baru",
maxlines: 2, color: GrayColors.gray600,
), fontWeight: FontWeight.w400,
SizedBox(height: 32.h), maxlines: 2,
TypographyStyles.body('Nama Lengkap', color: GrayColors.gray600, fontWeight: FontWeight.w400), ),
SizedBox(height: 16.w), SizedBox(height: 32.h),
TextFieldComponent(hintText: 'Masukkan nama lengkap'), TypographyStyles.body('Nama Lengkap', color: GrayColors.gray600, fontWeight: FontWeight.w400),
SizedBox(height: 20.h), SizedBox(height: 16.w),
Row( TextFieldComponent(
crossAxisAlignment: CrossAxisAlignment.start, controller: _nameController,
children: [ hintText: 'Masukkan nama lengkap',
Column( validators: Validators.validatorName,
crossAxisAlignment: CrossAxisAlignment.start, inputFormatters: [
children: [ FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\s]')),
TypographyStyles.body('Tipe ID', color: GrayColors.gray600, fontWeight: FontWeight.w400), UpperCaseTextFormatter(),
SizedBox(height: 16.h), ],
DropdownComponent( textInputType: TextInputType.text,
hintText: "Pilih jenis dokument", ),
value: "KTP", SizedBox(height: 20.h),
onChanged: (value) {}, Row(
), crossAxisAlignment: CrossAxisAlignment.start,
], children: [
), Column(
SizedBox(width: 16.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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), SizedBox(height: 16.h),
TextFieldComponent(hintText: 'Masukkan ID') DropdownComponent(
hintText: "Pilih jenis dokument",
items: ['KTP', 'Pasport'],
value: selectedTypeId,
onChanged: (value) {
setState(() {
selectedTypeId = value!;
});
},
),
], ],
), ),
) SizedBox(width: 16.w),
], Expanded(
), child: Column(
SizedBox(height: 20.w), crossAxisAlignment: CrossAxisAlignment.start,
TypographyStyles.body('Jenis Kelamin', color: GrayColors.gray600, fontWeight: FontWeight.w400), children: [
Row( TypographyStyles.body('No ID', color: GrayColors.gray600, fontWeight: FontWeight.w400),
children: [ SizedBox(height: 16.h),
_buildRadioButton(context, label: 'Laki-laki', value: 'Laki-laki'), TextFieldComponent(
SizedBox(width: 40.h), controller: _noIdController,
_buildRadioButton(context, label: 'Perempuan', value: 'Perempuan') 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( child: ButtonFill(
text: 'Simpan', text: 'Simpan',
textColor: Colors.white, 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/auth_binding.dart';
import 'package:e_porter/domain/bindings/navigation_binding.dart'; import 'package:e_porter/domain/bindings/navigation_binding.dart';
import 'package:e_porter/domain/bindings/profil_binding.dart';
import 'package:e_porter/domain/bindings/search_flight_binding.dart'; import 'package:e_porter/domain/bindings/search_flight_binding.dart';
import 'package:e_porter/domain/bindings/ticket_binding.dart'; import 'package:e_porter/domain/bindings/ticket_binding.dart';
import 'package:e_porter/presentation/screens/auth/pages/forget_password_screen.dart'; import 'package:e_porter/presentation/screens/auth/pages/forget_password_screen.dart';
@ -116,6 +117,7 @@ class AppRoutes {
GetPage( GetPage(
name: Routes.ADDPASSENGER, name: Routes.ADDPASSENGER,
page: () => AddPassengerScreen(), page: () => AddPassengerScreen(),
binding: ProfilBinding(),
), ),
]; ];
} }