Feat: done logic search bar
This commit is contained in:
parent
bd52aed744
commit
381f4f6a8f
|
@ -434,7 +434,7 @@
|
|||
"languageVersion": "3.4"
|
||||
}
|
||||
],
|
||||
"generated": "2025-03-05T07:39:52.656986Z",
|
||||
"generated": "2025-03-07T08:39:17.979426Z",
|
||||
"generator": "pub",
|
||||
"generatorVersion": "3.5.0",
|
||||
"flutterRoot": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0",
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
|
@ -1,4 +1,4 @@
|
|||
#Wed Mar 05 14:40:52 WIB 2025
|
||||
#Fri Mar 07 15:39:47 WIB 2025
|
||||
com.example.e_porter.app-main-46\:/drawable-v21/launch_background.xml=D\:\\Flutter\\Flutter Project\\e_porter\\build\\app\\intermediates\\merged_res\\debug\\drawable-v21_launch_background.xml.flat
|
||||
com.example.e_porter.app-main-46\:/mipmap-hdpi/ic_launcher.png=D\:\\Flutter\\Flutter Project\\e_porter\\build\\app\\intermediates\\merged_res\\debug\\mipmap-hdpi_ic_launcher.png.flat
|
||||
com.example.e_porter.app-main-46\:/mipmap-mdpi/ic_launcher.png=D\:\\Flutter\\Flutter Project\\e_porter\\build\\app\\intermediates\\merged_res\\debug\\mipmap-mdpi_ic_launcher.png.flat
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#Thu Mar 06 15:36:28 WIB 2025
|
||||
#Sat Mar 08 16:02:32 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
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -5,10 +5,12 @@ import 'package:flutter_svg/flutter_svg.dart';
|
|||
|
||||
class SearchBarComponent extends StatelessWidget {
|
||||
final String hintText;
|
||||
final ValueChanged<String>? onChanged;
|
||||
|
||||
const SearchBarComponent({
|
||||
Key? key,
|
||||
required this.hintText,
|
||||
this.onChanged
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -18,6 +20,7 @@ class SearchBarComponent extends StatelessWidget {
|
|||
color: Colors.white,
|
||||
),
|
||||
child: TextField(
|
||||
onChanged: onChanged,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Padding(
|
||||
padding: EdgeInsets.only(left: 16.w, right: 11.w),
|
||||
|
|
|
@ -26,6 +26,7 @@ class PreferencesService {
|
|||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
if (now > expiredAt) {
|
||||
await clearUserData();
|
||||
print("now: $now, expiredAt: $expiredAt");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -36,7 +37,7 @@ class PreferencesService {
|
|||
final userData = UserData.fromMap(map);
|
||||
return userData;
|
||||
}
|
||||
|
||||
|
||||
static Future<void> clearUserData() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_userDataKey);
|
||||
|
|
|
@ -3,6 +3,11 @@ import 'package:e_porter/domain/models/user_entity.dart';
|
|||
import 'package:e_porter/domain/repositories/auth_repository.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
|
||||
class AuthException implements Exception {
|
||||
final String message;
|
||||
AuthException(this.message);
|
||||
}
|
||||
|
||||
class AuthRepositoryImpl implements AuthRepository {
|
||||
final FirebaseAuth _firebaseAuth;
|
||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
|
@ -19,7 +24,21 @@ class AuthRepositoryImpl implements AuthRepository {
|
|||
final user = userCredential.user!;
|
||||
return UserEntity(uid: user.uid, email: user.email ?? "");
|
||||
} on FirebaseAuthException catch (e) {
|
||||
rethrow;
|
||||
print("FirebaseAuthException code: ${e.code}");
|
||||
print("FirebaseAuthException message: ${e.message}");
|
||||
|
||||
switch (e.code) {
|
||||
case 'invalid-email':
|
||||
throw AuthException("Format email tidak valid.");
|
||||
case 'user-not-found':
|
||||
throw AuthException("Email tidak terdaftar.");
|
||||
case 'wrong-password':
|
||||
throw AuthException("Password salah.");
|
||||
case 'invalid-credential':
|
||||
throw AuthException("Email tidak terdaftar atau password salah.");
|
||||
default:
|
||||
throw AuthException(e.message ?? "Terjadi kesalahan.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
class UserEntity {
|
||||
final String uid;
|
||||
final String email;
|
||||
|
@ -9,15 +11,15 @@ class UserEntity {
|
|||
}
|
||||
|
||||
class UserData {
|
||||
final String name;
|
||||
final String email;
|
||||
final String phone;
|
||||
final String birthDate;
|
||||
final String gender;
|
||||
final String work;
|
||||
final String city;
|
||||
final String address;
|
||||
final String role;
|
||||
final String? name;
|
||||
final String? email;
|
||||
final String? phone;
|
||||
final String? birthDate;
|
||||
final String? gender;
|
||||
final String? work;
|
||||
final String? city;
|
||||
final String? address;
|
||||
final String? role;
|
||||
|
||||
UserData({
|
||||
required this.name,
|
||||
|
@ -32,19 +34,30 @@ class UserData {
|
|||
});
|
||||
|
||||
factory UserData.fromMap(Map<String, dynamic> map) {
|
||||
final timestamp = map['birth_date'];
|
||||
String? birthDateString;
|
||||
|
||||
if (timestamp is Timestamp) {
|
||||
// Konversi ke DateTime lalu ke String (format bebas)
|
||||
birthDateString = timestamp.toDate().toIso8601String();
|
||||
} else if (timestamp is String) {
|
||||
// Kalau sudah String, langsung pakai
|
||||
birthDateString = timestamp;
|
||||
}
|
||||
|
||||
return UserData(
|
||||
name: map['name'],
|
||||
email: map['email'],
|
||||
phone: map['phone'],
|
||||
birthDate: map['birth_date'],
|
||||
gender: map['gender'],
|
||||
work: map['work'],
|
||||
city: map['city'],
|
||||
address: map['address'],
|
||||
role: map['role'],
|
||||
name: map['name'] as String?,
|
||||
email: map['email'] as String?,
|
||||
phone: map['phone'] as String?,
|
||||
birthDate: birthDateString,
|
||||
gender: map['gender'] as String?,
|
||||
work: map['work'] as String?,
|
||||
city: map['city'] as String?,
|
||||
address: map['address'] as String?,
|
||||
role: map['role'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'name': name,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
|
||||
import 'package:e_porter/data/repositories/auth_repository_impl.dart';
|
||||
import 'package:e_porter/domain/usecases/auth_usecase.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
|
@ -57,7 +56,7 @@ class AuthController extends GetxController {
|
|||
return;
|
||||
}
|
||||
|
||||
if (userData.role.toLowerCase() != effectiveRole.toLowerCase()) {
|
||||
if (userData.role!.toLowerCase() != effectiveRole.toLowerCase()) {
|
||||
_showErrorSnackbar("Role Tidak Sesuai",
|
||||
"Data user menunjukkan role '${userData.role}', bukan '$effectiveRole'.");
|
||||
return;
|
||||
|
@ -66,28 +65,8 @@ class AuthController extends GetxController {
|
|||
await PreferencesService.saveUserData(userData);
|
||||
Get.offAllNamed(Routes.NAVBAR, arguments: effectiveRole);
|
||||
|
||||
} on FirebaseAuthException catch (e) {
|
||||
print("FirebaseAuthException code: ${e.code}");
|
||||
switch (e.code) {
|
||||
case 'user-not-found':
|
||||
_showErrorSnackbar("Login Gagal", "Email belum terdaftar.");
|
||||
break;
|
||||
case 'wrong-password':
|
||||
_showErrorSnackbar("Login Gagal", "Password salah.");
|
||||
break;
|
||||
case 'invalid-email':
|
||||
_showErrorSnackbar("Login Gagal", "Format email tidak valid.");
|
||||
break;
|
||||
case 'invalid-credential':
|
||||
if (e.message != null && e.message!.toLowerCase().contains('password')) {
|
||||
_showErrorSnackbar("Login Gagal", "Password salah.");
|
||||
} else {
|
||||
_showErrorSnackbar("Login Gagal", "Email belum terdaftar.");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
_showErrorSnackbar("Login Gagal", e.message ?? "Terjadi kesalahan.");
|
||||
}
|
||||
} on AuthException catch (e) {
|
||||
_showErrorSnackbar("Login Gagal", e.message);
|
||||
} catch (e) {
|
||||
_showErrorSnackbar("Terjadi Kesalahan", e.toString());
|
||||
} finally {
|
||||
|
|
|
@ -5,7 +5,10 @@ import '../../domain/usecases/get_airport.dart';
|
|||
|
||||
class SearchFlightController extends GetxController {
|
||||
final GetAirports getAirports;
|
||||
|
||||
var airports = <Airport>[].obs; // Gunakan Rx agar bisa reaktif
|
||||
var filteredAirports = <Airport>[].obs;
|
||||
var searchText = ''.obs;
|
||||
|
||||
SearchFlightController(this.getAirports);
|
||||
|
||||
|
@ -13,24 +16,27 @@ class SearchFlightController extends GetxController {
|
|||
void onInit() {
|
||||
super.onInit();
|
||||
fetchAirports();
|
||||
debounce(searchText, (_) => searchAirports(searchText.value), time: Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
void fetchAirports() async {
|
||||
final result = await getAirports();
|
||||
airports.value = result;
|
||||
filteredAirports.assignAll(result);
|
||||
}
|
||||
|
||||
// Misalnya, jika Anda ingin menambahkan pencarian:
|
||||
void searchAirports(String query) {
|
||||
// Contoh filter sederhana
|
||||
final filtered = airports.where((airport) {
|
||||
final city = airport.city.toLowerCase();
|
||||
final code = airport.code.toLowerCase();
|
||||
final name = airport.name.toLowerCase();
|
||||
final q = query.toLowerCase();
|
||||
return city.contains(q) || code.contains(q) || name.contains(q);
|
||||
}).toList();
|
||||
final lowerCaseQuery = query.toLowerCase();
|
||||
|
||||
airports.value = filtered;
|
||||
if (lowerCaseQuery.isEmpty) {
|
||||
filteredAirports.assignAll(airports);
|
||||
} else {
|
||||
filteredAirports.assignAll(airports
|
||||
.where((airport) =>
|
||||
airport.name.toLowerCase().contains(lowerCaseQuery) ||
|
||||
airport.city.toLowerCase().contains(lowerCaseQuery) ||
|
||||
airport.code.toLowerCase().contains(lowerCaseQuery))
|
||||
.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,6 +69,7 @@ class _BoardingPassScreenState extends State<BoardingPassScreen> with SingleTick
|
|||
padding: EdgeInsets.only(bottom: 16.h),
|
||||
child: CardBoardingPass(
|
||||
isActive: false,
|
||||
onTap: () {},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -78,7 +79,10 @@ class _BoardingPassScreenState extends State<BoardingPassScreen> with SingleTick
|
|||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 16.h),
|
||||
child: CardBoardingPass(isActive: true),
|
||||
child: CardBoardingPass(
|
||||
isActive: true,
|
||||
onTap: () {},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
@ -4,6 +4,7 @@ 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/domain/models/airport.dart';
|
||||
import 'package:e_porter/presentation/screens/home/component/flight_class_radio.dart';
|
||||
import 'package:e_porter/presentation/screens/home/component/flight_date_selector.dart';
|
||||
import 'package:e_porter/presentation/screens/home/component/flight_selector.dart';
|
||||
|
@ -25,9 +26,11 @@ class BookingTickets extends StatefulWidget {
|
|||
class _BookingTicketsState extends State<BookingTickets> {
|
||||
DateTime selectedDate = DateTime.now();
|
||||
String selectedDateText = 'dd/mm/yyyy';
|
||||
|
||||
final ValueNotifier<String> selectedClass = ValueNotifier<String>('Economy');
|
||||
|
||||
Airport? selectedAirportFrom;
|
||||
Airport? selectedAirportTo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -51,19 +54,33 @@ class _BookingTicketsState extends State<BookingTickets> {
|
|||
children: [
|
||||
FlightSelector(
|
||||
label: 'Dari',
|
||||
hintText: 'Pilih Bandara',
|
||||
hintText: selectedAirportFrom == null
|
||||
? 'Pilih Bandara'
|
||||
: '${selectedAirportFrom!.code} - ${selectedAirportFrom!.city}',
|
||||
svgIconPath: CustomeIcons.PlaneRightOutline(),
|
||||
onTap: () {
|
||||
Get.toNamed(Routes.SEARCHFLIGHT);
|
||||
onTap: () async {
|
||||
final result = await Get.toNamed(Routes.SEARCHFLIGHT);
|
||||
if (result != null) {
|
||||
setState(() {
|
||||
selectedAirportFrom = result;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
FlightSelector(
|
||||
label: 'Ke',
|
||||
hintText: 'Pilih Bandara',
|
||||
hintText: selectedAirportTo == null
|
||||
? 'Pilih Bandara'
|
||||
: '${selectedAirportTo!.code} - ${selectedAirportTo!.city}',
|
||||
svgIconPath: CustomeIcons.PlaneLeftOutline(),
|
||||
onTap: () {
|
||||
Get.toNamed(Routes.SEARCHFLIGHT);
|
||||
onTap: () async {
|
||||
final result = await Get.toNamed(Routes.SEARCHFLIGHT);
|
||||
if (result != null) {
|
||||
setState(() {
|
||||
selectedAirportTo = result;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
|
|
|
@ -250,7 +250,28 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
],
|
||||
),
|
||||
SizedBox(height: 32.w),
|
||||
CustomeShadowCotainner(child: TypographyStyles.body('Mulai Antrian'))
|
||||
CustomeShadowCotainner(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h),
|
||||
decoration: BoxDecoration(
|
||||
color: PrimaryColors.primary200,
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
child: SvgPicture.asset(
|
||||
'assets/icons/ic_account.svg',
|
||||
width: 32.w,
|
||||
height: 32.h,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
TypographyStyles.body(
|
||||
'Mulai Antrian',
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -34,7 +34,12 @@ class _SearchFlightScreenState extends State<SearchFlightScreen> {
|
|||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h),
|
||||
child: Column(
|
||||
children: [
|
||||
SearchBarComponent(hintText: 'Cari Bandara'),
|
||||
SearchBarComponent(
|
||||
hintText: 'Cari Bandara',
|
||||
onChanged: (value) {
|
||||
controller.searchText.value = value;
|
||||
},
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
Expanded(
|
||||
child: Container(
|
||||
|
@ -45,7 +50,11 @@ class _SearchFlightScreenState extends State<SearchFlightScreen> {
|
|||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
child: Obx(() {
|
||||
final airports = controller.airports;
|
||||
final airports = controller.filteredAirports;
|
||||
|
||||
if (airports.isEmpty) {
|
||||
return _buildIsEmpty();
|
||||
}
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: airports.length,
|
||||
|
@ -54,7 +63,9 @@ class _SearchFlightScreenState extends State<SearchFlightScreen> {
|
|||
return _buildCardItem(
|
||||
'${airport.code} - ${airport.city}',
|
||||
'${airport.name}',
|
||||
() {},
|
||||
() {
|
||||
Get.back(result: airport);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -100,4 +111,16 @@ class _SearchFlightScreenState extends State<SearchFlightScreen> {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIsEmpty() {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 16.w),
|
||||
width: double.infinity,
|
||||
child: TypographyStyles.caption(
|
||||
"Data tidak ditemukan",
|
||||
color: GrayColors.gray400,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue