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,15 +36,20 @@ 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(
Icons.edit, onTap: (){
color: Constant.bgButton, print('id : ${data.personID}');
size: 20, },
child: Icon(
Icons.edit,
color: Constant.bgButton,
size: 20,
),
), ),
], ],
), ),
@@ -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,136 +76,202 @@ class HomeScreen extends HookConsumerWidget {
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
backgroundColor: Constant.bgGrey, backgroundColor: Constant.bgGrey,
body: SingleChildScrollView( body: Column(
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, // atas
children: [ Image.asset(
// atas 'images/vektoratas.png',
Image.asset( width: double.infinity,
'images/vektoratas.png', fit: BoxFit.cover,
),
SizedBox(
height: Constant.getActualYPhone(
context: context,
y: 34,
),
),
// button scan ktp
Padding(
padding: EdgeInsets.only(
left: Constant.getActualXPhone(
context: context,
x: 20,
),
right: Constant.getActualXPhone(
context: context,
x: 20,
),
),
child: SizedBox(
width: double.infinity, width: double.infinity,
fit: BoxFit.cover,
),
SizedBox(
height: Constant.getActualYPhone( height: Constant.getActualYPhone(
context: context, context: context,
y: 34, y: 48,
), ),
), child: ElevatedButton(
// button scan ktp onPressed: () {},
Padding( style: ElevatedButton.styleFrom(
padding: EdgeInsets.only( backgroundColor: Constant.bgButton,
left: Constant.getActualXPhone( shape: RoundedRectangleBorder(
context: context, borderRadius: BorderRadius.circular(8),
x: 20, ),
elevation: 8,
shadowColor: Constant.bgButton.withOpacity(0.24),
), ),
right: Constant.getActualXPhone( child: Row(
context: context, mainAxisAlignment: MainAxisAlignment.center,
x: 20, children: [
), Image.asset(
), 'images/iconbarcode.png',
child: SizedBox( width: Constant.getActualXPhone(
width: double.infinity, context: context,
height: Constant.getActualYPhone( x: 20,
context: context, ),
y: 48, height: Constant.getActualYPhone(
), context: context,
child: ElevatedButton( y: 20,
onPressed: () {}, ),
style: ElevatedButton.styleFrom( fit: BoxFit.cover,
backgroundColor: Constant.bgButton,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
), ),
elevation: 8, SizedBox(
shadowColor: Constant.bgButton.withOpacity(0.24), width: Constant.getActualXPhone(
), context: context,
child: Row( x: 10,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'images/iconbarcode.png',
width: Constant.getActualXPhone(
context: context,
x: 20,
),
height: Constant.getActualYPhone(
context: context,
y: 20,
),
fit: BoxFit.cover,
), ),
SizedBox( ),
width: Constant.getActualXPhone( Text(
context: context, 'SCAN KTP BARU',
x: 10, style:
), Constant.titleButton500(context: context).copyWith(
color: Constant.textWhite,
), ),
Text( ),
'SCAN KTP BARU', ],
style: Constant.titleButton500(context: context)
.copyWith(
color: Constant.textWhite,
),
),
],
),
), ),
), ),
), ),
SizedBox( ),
height: Constant.getActualYPhone( SizedBox(
height: Constant.getActualYPhone(
context: context,
y: 56,
),
),
// judul
Padding(
padding: EdgeInsets.only(
left: Constant.getActualXPhone(
context: context, context: context,
y: 56, x: 20,
),
right: Constant.getActualXPhone(
context: context,
x: 20,
), ),
), ),
Padding( child: Column(
padding: EdgeInsets.only( children: [
left: Constant.getActualXPhone( Row(
context: context, mainAxisAlignment: MainAxisAlignment.spaceBetween,
x: 20, children: [
), Text(
right: Constant.getActualXPhone( 'Riwayat',
context: context, style: Constant.titleRiwayat(context: context).copyWith(
x: 20, color: Constant.textBlack,
),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Riwayat',
style:
Constant.titleRiwayat(context: context).copyWith(
color: Constant.textBlack,
),
), ),
Text(
'20 Scan Terakhir',
style: Constant.titleInputan500(context: context)
.copyWith(
color: Constant.inputanGrey,
),
),
],
),
SizedBox(
height: Constant.getActualYPhone(
context: context,
y: 24,
), ),
Text(
'20 Scan Terakhir',
style:
Constant.titleInputan500(context: context).copyWith(
color: Constant.inputanGrey,
),
),
],
),
SizedBox(
height: Constant.getActualYPhone(
context: context,
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'],