step 9 : listing history scan dan pull to refresh, perbaikan auth model add host

This commit is contained in:
sindhu
2025-02-15 22:04:19 +07:00
parent 32abcb60dc
commit e90c710333
10 changed files with 378 additions and 122 deletions

View File

@@ -0,0 +1,5 @@
import 'package:jiffy/jiffy.dart';
String formatDateJiffy(String serverDate) {
return Jiffy.parse(serverDate).format(pattern: 'dd-MM-yyyy');
}

View File

@@ -1,14 +1,17 @@
class AuthModel { class AuthModel {
final String token; final String token;
final String host;
final UserModel model; final UserModel model;
AuthModel({ AuthModel({
required this.host,
required this.token, required this.token,
required this.model, required this.model,
}); });
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'host':host,
'token': token, 'token': token,
'model': model.toJson(), 'model': model.toJson(),
}; };

View File

@@ -0,0 +1,41 @@
class PersonKtp {
final String personID;
final String personNIK;
final String personName;
final String personDob;
final String personSex;
final String personUrl;
PersonKtp({
required this.personID,
required this.personNIK,
required this.personName,
required this.personDob,
required this.personSex,
required this.personUrl,
});
// Convert JSON to Model
factory PersonKtp.fromJson(Map<String, dynamic> json) {
return PersonKtp(
personID: json['Person_ID'],
personNIK: json['Person_NIK'],
personName: json['Person_Name'],
personDob: json['Person_Dob'],
personSex: json['Person_Sex'],
personUrl: json['Person_Url'],
);
}
// Convert Model to JSON
Map<String, dynamic> toJson() {
return {
'Person_ID':personID,
'Person_NIK': personNIK,
'Person_Name': personName,
'Person_Dob': personDob,
'Person_Sex': personSex,
'Person_Url': personUrl,
};
}
}

View File

@@ -11,10 +11,11 @@ class AuthRepository extends BaseRepository {
}) async { }) async {
final param = {"username": username, "password": password}; final param = {"username": username, "password": password};
// final service = "${Constant.baseUrl}xauth/login"; // final service = "${Constant.baseUrl}xauth/login";
final service = "http://${host}/one-api/v1/system/auth/login"; final service = "http://$host/one-api/v1/system/auth/login";
final resp = await post(param: param, service: service); final resp = await post(param: param, service: service);
final result = AuthModel( final result = AuthModel(
host: host,
token: resp["data"]["token"], token: resp["data"]["token"],
model: UserModel.fromJson(resp["data"]["user"]), model: UserModel.fromJson(resp["data"]["user"]),
); );

View File

@@ -0,0 +1,22 @@
import '../model/person_ktp_model.dart';
import 'base_repository.dart';
class ScanRepository extends BaseRepository {
ScanRepository({required super.dio});
Future<List<PersonKtp>> listRiwayatScanRepo({
required String host,
}) async {
// final service = "${Constant.baseUrl}xauth/login";
final service = "http://${host}/one-api/scan-ktp/Scanktp/listRiwayatScan";
final resp = await post(param: {}, service: service);
final result = List<PersonKtp>.empty(growable: true);
resp['data'].forEach((e) {
final model = PersonKtp.fromJson(e);
result.add(model);
});
return result;
}
}

View File

@@ -1,11 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:scanktpflutter/app/app_extension.dart';
import '../../app/constant.dart'; import '../../app/constant.dart';
import '../../model/person_ktp_model.dart';
class CardRiwayatScan extends HookConsumerWidget { class CardRiwayatScan extends HookConsumerWidget {
const CardRiwayatScan({super.key}); final PersonKtp data;
const CardRiwayatScan({
super.key,
required this.data,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -30,16 +36,21 @@ class CardRiwayatScan extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'3171234567890123', data.personNIK,
style: Constant.titleInputan600(context: context).copyWith( style: Constant.titleInputan600(context: context).copyWith(
color: Constant.textBlack, color: Constant.textBlack,
), ),
), ),
Icon( InkWell(
onTap: (){
print('id : ${data.personID}');
},
child: Icon(
Icons.edit, Icons.edit,
color: Constant.bgButton, color: Constant.bgButton,
size: 20, size: 20,
), ),
),
], ],
), ),
SizedBox( SizedBox(
@@ -63,7 +74,7 @@ class CardRiwayatScan extends HookConsumerWidget {
), ),
), ),
Text( Text(
'MIRA SETIAWAN', data.personName,
style: Constant.cardText(context: context).copyWith( style: Constant.cardText(context: context).copyWith(
color: Constant.textCardGrey, color: Constant.textCardGrey,
), ),
@@ -92,7 +103,7 @@ class CardRiwayatScan extends HookConsumerWidget {
), ),
), ),
Text( Text(
'18-02-1996', formatDateJiffy(data.personDob),
style: Constant.cardText(context: context).copyWith( style: Constant.cardText(context: context).copyWith(
color: Constant.textCardGrey, color: Constant.textCardGrey,
), ),
@@ -128,7 +139,7 @@ class CardRiwayatScan extends HookConsumerWidget {
), ),
), ),
Text( Text(
'Perempuan', data.personSex,
style: Constant.cardText(context: context).copyWith( style: Constant.cardText(context: context).copyWith(
color: Constant.textCardGrey, color: Constant.textCardGrey,
), ),

View File

@@ -2,11 +2,14 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:scanktpflutter/model/person_ktp_model.dart';
import 'package:scanktpflutter/screen/home/card_riwayat_scan.dart'; import 'package:scanktpflutter/screen/home/card_riwayat_scan.dart';
import 'package:scanktpflutter/screen/home/list_riwayat_scan_provider.dart';
import '../../app/constant.dart'; import '../../app/constant.dart';
import '../../app/route.dart'; import '../../app/route.dart';
import '../../provider/current_user_provider.dart'; import '../../provider/current_user_provider.dart';
import '../../widget/customsnackbarwidget.dart';
class HomeScreen extends HookConsumerWidget { class HomeScreen extends HookConsumerWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
@@ -18,6 +21,12 @@ class HomeScreen extends HookConsumerWidget {
]); ]);
final currentUser = ref.watch(currentUserProvider); final currentUser = ref.watch(currentUserProvider);
// final username = currentUser?.model.username ?? "-";
final host = currentUser?.host ?? "";
final isLoading = useState<bool>(false);
final listScanArr = useState<List<PersonKtp>>(
List.empty(growable: true),
);
useEffect(() { useEffect(() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
@@ -32,7 +41,33 @@ class HomeScreen extends HookConsumerWidget {
return () {}; return () {};
}, [currentUser]); }, [currentUser]);
final username = currentUser?.model.username ?? "-"; // listRiwayProvider
ref.listen(listRiwayatScanProvider, (prev, next) {
if (next is ListRiwayatScanStateLoading) {
isLoading.value = true;
} else if (next is ListRiwayatScanStateError) {
isLoading.value = false;
// errorMessage.value = next.message;
snackbarWidget(
context,
next.message,
snackbarType.error,
Duration(seconds: 3),
);
} else if (next is ListRiwayatScanStateDone) {
isLoading.value = false;
listScanArr.value = next.model;
}
});
useEffect(() {
WidgetsBinding.instance.addPostFrameCallback((timestap) async {
ref.read(listRiwayatScanProvider.notifier).listRiwayatScan(
host: host,
);
});
return () {};
}, []);
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
@@ -41,9 +76,7 @@ class HomeScreen extends HookConsumerWidget {
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
backgroundColor: Constant.bgGrey, backgroundColor: Constant.bgGrey,
body: SingleChildScrollView( body: Column(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// atas // atas
Image.asset( Image.asset(
@@ -108,8 +141,8 @@ class HomeScreen extends HookConsumerWidget {
), ),
Text( Text(
'SCAN KTP BARU', 'SCAN KTP BARU',
style: Constant.titleButton500(context: context) style:
.copyWith( Constant.titleButton500(context: context).copyWith(
color: Constant.textWhite, color: Constant.textWhite,
), ),
), ),
@@ -124,6 +157,7 @@ class HomeScreen extends HookConsumerWidget {
y: 56, y: 56,
), ),
), ),
// judul
Padding( Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: Constant.getActualXPhone( left: Constant.getActualXPhone(
@@ -142,15 +176,14 @@ class HomeScreen extends HookConsumerWidget {
children: [ children: [
Text( Text(
'Riwayat', 'Riwayat',
style: style: Constant.titleRiwayat(context: context).copyWith(
Constant.titleRiwayat(context: context).copyWith(
color: Constant.textBlack, color: Constant.textBlack,
), ),
), ),
Text( Text(
'20 Scan Terakhir', '20 Scan Terakhir',
style: Constant.titleInputan500(context: context) style:
.copyWith( Constant.titleInputan500(context: context).copyWith(
color: Constant.inputanGrey, color: Constant.inputanGrey,
), ),
), ),
@@ -162,16 +195,84 @@ class HomeScreen extends HookConsumerWidget {
y: 24, y: 24,
), ),
), ),
// card riwayat
CardRiwayatScan(),
// card riwayat
], ],
), ),
), ),
// loading
if (isLoading.value)
Expanded(
child: Center(
child: CircularProgressIndicator(
color: Constant.bgButton,
),
),
)
// belum ada riwayat
else if (listScanArr.value.isEmpty)
Expanded(
child: RefreshIndicator(
color: Constant.bgButton,
onRefresh: () async {
ref.read(listRiwayatScanProvider.notifier).listRiwayatScan(
host: host,
);
},
child: ListView(
physics:
AlwaysScrollableScrollPhysics(), // Agar bisa di-refresh meski kosong
children: [
SizedBox(
height: MediaQuery.of(context).size.height *
0.3), // Atur tinggi agar teks di tengah
Align(
alignment: Alignment.center,
child: Text(
'Belum Ada Riwayat',
style: Constant.titleInputan500(context: context),
),
),
], ],
), ),
), ),
)
// list card
else
Expanded(
child: RefreshIndicator(
color: Constant.bgButton,
onRefresh: () async {
ref.read(listRiwayatScanProvider.notifier).listRiwayatScan(
host: host,
);
},
child: Padding(
padding: EdgeInsets.only(
left: Constant.getActualXPhone(context: context, x: 13),
right: Constant.getActualXPhone(context: context, x: 13),
bottom: Constant.getActualYPhone(context: context, y: 20),
),
child: ListView.builder(
physics: AlwaysScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: listScanArr.value.length,
itemBuilder: (context, i) {
final obj = listScanArr.value[i];
return Padding(
padding: EdgeInsets.only(
top: Constant.getActualYPhone(
context: context, y: 15),
),
child: CardRiwayatScan(
data: obj,
),
);
},
),
),
),
),
],
),
), ),
); );
} }

View File

@@ -0,0 +1,70 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:scanktpflutter/repository/scan_repository.dart';
import '../../model/person_ktp_model.dart';
import '../../provider/dio_provider.dart';
import '../../repository/base_repository.dart';
// 3. state provider
final listRiwayatScanProvider =
StateNotifierProvider<ListRiwayatScanNotifier, ListRiwayatScanState>(
(ref) => ListRiwayatScanNotifier(ref: ref));
// 2. notifier
class ListRiwayatScanNotifier extends StateNotifier<ListRiwayatScanState> {
final Ref ref;
ListRiwayatScanNotifier({required this.ref})
: super(ListRiwayatScanStateInit());
void listRiwayatScan({
required String host,
}) async {
try {
state = ListRiwayatScanStateLoading();
final resp = await ScanRepository(
dio: ref.read(dioProvider),
).listRiwayatScanRepo(
host: host,
);
// print(resp);
state = ListRiwayatScanStateDone(model: resp);
} catch (e) {
if (e is BaseRepositoryException) {
state = ListRiwayatScanStateError(message: e.message);
} else {
state = ListRiwayatScanStateError(message: e.toString());
}
}
}
}
// 1. state
abstract class ListRiwayatScanState extends Equatable {
final DateTime date;
const ListRiwayatScanState(this.date);
@override
List<Object?> get props => [date];
}
class ListRiwayatScanStateInit extends ListRiwayatScanState {
ListRiwayatScanStateInit() : super(DateTime.now());
}
class ListRiwayatScanStateLoading extends ListRiwayatScanState {
ListRiwayatScanStateLoading() : super(DateTime.now());
}
class ListRiwayatScanStateError extends ListRiwayatScanState {
final String message;
ListRiwayatScanStateError({
required this.message,
}) : super(DateTime.now());
}
class ListRiwayatScanStateDone extends ListRiwayatScanState {
final List<PersonKtp> model;
ListRiwayatScanStateDone({
required this.model,
}) : super(DateTime.now());
}

View File

@@ -39,6 +39,7 @@ class LoginNotifier extends StateNotifier<LoginState> {
//Simpan ke token //Simpan ke token
final shared = await SharedPreferences.getInstance(); final shared = await SharedPreferences.getInstance();
final token = jsonEncode({ final token = jsonEncode({
"host":host,
"date": DateTime.now().toString(), "date": DateTime.now().toString(),
"model": resp.model, "model": resp.model,
"token": resp.token "token": resp.token

View File

@@ -40,6 +40,7 @@ class SplashScreen extends HookConsumerWidget {
if (xmodel == null) return; if (xmodel == null) return;
final authModel = AuthModel( final authModel = AuthModel(
host: xmodel["host"],
token: xmodel["token"], token: xmodel["token"],
model: UserModel( model: UserModel(
userId: xmodel["model"]['M_UserID'], userId: xmodel["model"]['M_UserID'],