From a0d383ea6c1a67fdfeba67b2af834cbf7e9facd0 Mon Sep 17 00:00:00 2001 From: sindhu Date: Fri, 26 Jan 2024 16:47:32 +0700 Subject: [PATCH] step 12 : proses absen masuk selfie, absen pulang selfie --- android/app/build.gradle | 2 + android/app/src/main/AndroidManifest.xml | 9 +- images/camera_selfie.png | Bin 0 -> 711 bytes images/warning_selfie.png | Bin 0 -> 429 bytes lib/app/constant.dart | 11 + lib/main.dart | 2 +- lib/screen/home/home_screen.dart | 6 +- lib/screen/presensi/presensi_screen.dart | 2 +- .../presensi/presensi_selfie_screen.dart | 913 +++++++++++++++++- .../presensi/presensi_selfie_upload_area.dart | 181 ++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 208 ++++ pubspec.yaml | 7 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 17 files changed, 1340 insertions(+), 12 deletions(-) create mode 100644 images/camera_selfie.png create mode 100644 images/warning_selfie.png create mode 100644 lib/screen/presensi/presensi_selfie_upload_area.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index c95be58..ccb8226 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -53,6 +53,7 @@ android { targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName + multiDexEnabled true } buildTypes { @@ -71,4 +72,5 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation platform('com.google.firebase:firebase-bom:32.7.0') + implementation 'com.android.support:multidex:1.0.3' } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8925ec9..d8430f4 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,10 @@ - - - + + + + + + kSwK#} zC#l4CO4I2XkZS}gODw$e+em6n_v?B6W&j?M0?e{#Rxi~6xopQAgh~F>RVOVsmEhM& z@x21_nRygTL@X=j4_`X9w^Q;Vx;{qy|(CH&{GvAAMJ9}HK5@BB{ejUZU&sT$c z2_&f?Ntg0kh++&da=YUSBw$%DT{1$|3QdLZScs;H1Uf+>%=ypXQ}1FrhaI4sbBNl& z?B;vp<0zi97wK+TcaTurNRq=>(xC_c!VB-qkJwCf*J6eaA@uDyK?W=vrM8xi6*Q6Y zh@Rwz%f`$0)XygwCJHKk)eEkBijkXi_QZaYNFp_Dp< z4eP@G{)ia{b|?qwD1;0@6XF9x^cRg6eLYzDp5pE8AY?v`H3;dL0#Vft{H_<@kwZiA zU~666G{V45nbmM9|1%RPRv!ttrERFc^)n<2tiqsYVx_o@7)0w+3B2C-Kml~5vx%`P z;<;Vitg#MOL?U;IpNYT}=#G5_Sb^*7k;rnCB}dMKk!fSdIClsSlS#fA$1F}t1^v{r z*qtAC$L3a8UpEu8i%kN#9lr{*)^CwSVs;=+$cb`w5h1%PF7F&$%VLgA0@&O0)y++? zzVNny2M4tfxFfo@APvX^^MD<(l0Ym9aTl#+iK@@#AXc=5nTBlR?i3rTR!As*7R_hp t9Yiob7t7MhRRvR+xg!=15^Vc$_z%?i9bdm{&^7=7002ovPDHLkV1oGfGz9VV4oJL}Y)f_+V1PZ?(th4P z?LMME8h_?=YpuzhEs5Q#cfAnVn)jyr62NJ_pbNEwcSFt{5jG)9fH=dc2GD&9#7S2q zs3)A{Rv^jDSmj7BeaWl6VPlTsR|%P_FTNd$1vCI7x;M4Ne9jeE_NHQY-r`hE-zlJ~ z9B)463SeyfoFW62BhVqCp>NEylleHcN&sAj&w+ky7%tN?0rZi9&&w` -1); Navigator.pushNamed(context, presensiSelfieRoute); } else { - ref.read(currentPageProvider.notifier).update((state) => -1); - Navigator.pushNamed(context, presensiRoute); + if (varCurrentDistanceProvider?.selfie == "FALSE") { + ref.read(currentPageProvider.notifier).update((state) => -1); + Navigator.pushNamed(context, presensiRoute); + } } } }); diff --git a/lib/screen/presensi/presensi_screen.dart b/lib/screen/presensi/presensi_screen.dart index 7f1fed8..b4d4089 100644 --- a/lib/screen/presensi/presensi_screen.dart +++ b/lib/screen/presensi/presensi_screen.dart @@ -507,7 +507,7 @@ class PresensiScreen extends HookConsumerWidget { // tombol aksi absen masuk & pulang if (varCurrentCheckJamProvider?.isAbsenClockIn == "TRUE" && - varCurrentCheckJamProvider?.isAbsenClockIn == "TRUE") ...[ + varCurrentCheckJamProvider?.isAbsenClockOut == "TRUE") ...[ SizedBox.shrink() ] else ...[ if (varCurrentCheckJamProvider?.isAbsenClockIn == "FALSE" && diff --git a/lib/screen/presensi/presensi_selfie_screen.dart b/lib/screen/presensi/presensi_selfie_screen.dart index fed93e3..71a33b5 100644 --- a/lib/screen/presensi/presensi_selfie_screen.dart +++ b/lib/screen/presensi/presensi_selfie_screen.dart @@ -1,12 +1,480 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:absensi_sas_flutter/screen/presensi/presensi_clock_in_provider.dart'; +import 'package:absensi_sas_flutter/screen/presensi/presensi_clock_out_provider.dart'; +import 'package:absensi_sas_flutter/widget/custom_drawer.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:mobkit_dashed_border/mobkit_dashed_border.dart'; +import 'package:permission_handler/permission_handler.dart'; import '../../app/constant.dart'; +import '../../app/route.dart'; +import '../../provider/current_check_distance_provider.dart'; +import '../../provider/current_check_jam_presensi_provider.dart'; +import '../../provider/current_user_provider.dart'; +import '../../widget/real_date.dart'; +import '../../widget/real_time.dart'; +import '../../widget/sankbar_widget.dart'; +import 'check_distance_provider.dart'; +import 'check_presensi_jam_provider.dart'; +import 'presensi_selfie_upload_area.dart'; +import 'dart:io' as io; -class PresensiSelfieScreen extends StatelessWidget { +class PresensiSelfieScreen extends HookConsumerWidget { const PresensiSelfieScreen({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final currentAddressUserLocation = useState(""); + final isLoadingAddressUserLocation = useState(false); + final isLoadingProsesCheckDistance = useState(false); + final varCurrentDistanceProvider = ref.watch(currentCheckDistanceProvider); + final varCurrentCheckJamProvider = + ref.watch(currentCheckJamPresensiProvider); + final selectedUser = ref.watch(currentUserProvider); + + final positionLatitude = useState(""); + final positionLongitude = useState(""); + // final tTransactionCurrentDistance = useState("NULL"); + + final fileData = useState(null); + final fileDataBase64 = useState(""); + final isImage = useState(false); + final fileEkstension = useState(""); + final fileSize = useState(0); + final fileName = useState(""); + + getBase64() async { + // List imageBytes = await fileData.value?.readAsBytes(); + if (fileData.value != null) { + final bytes = io.File(fileData.value!.path).readAsBytesSync(); + String base64Image = base64Encode(await fileData.value!.readAsBytes()); + fileDataBase64.value = base64Image; + final file = File(fileData.value!.path); + + print(file.lengthSync() / 1000000); + print(await fileData.value!.length()); + fileSize.value = await fileData.value!.length(); + + // await getExternalStorageDirectory(); + // print(await fileData.value!.saveTo(path)); + print(fileDataBase64.value); + } + } + + final ImagePicker _picker = ImagePicker(); + pickImage() async { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource.camera, + // maxWidth set untuk width image + maxWidth: 640, + // maxHeight set untuk width image + maxHeight: 480); + if (pickedFile != null) { + final tmpFile = FilePickerResult([ + PlatformFile( + name: pickedFile.name, + size: await pickedFile.length(), + path: pickedFile.path, + ) + ]); + if (await pickedFile.length() > 10000000) { + SanckbarWidget(context, "File tidak boleh lebih dari 10 MB", + snackbarType.warning); + } else { + fileData.value = pickedFile; + isImage.value = true; + fileEkstension.value = tmpFile.files.single.extension ?? ""; + DateTime now = new DateTime.now(); + fileName.value = + "IMG-${now.year}${now.month}${now.day}.${tmpFile.files.single.extension ?? ''}"; + } + + // final Directory appDocumentsDir = + // await getApplicationDocumentsDirectory(); + // print(appDocumentsDir); + // await pickedFile!.saveTo(appDocumentsDir.path); + await getBase64(); + } + } + + browseImage() async { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource.gallery, + ); + fileData.value = pickedFile; + getBase64(); + } + + pickFile() async { + List imgExt = [ + 'jpg', + 'png', + 'jpeg', + ]; + FilePickerResult? result = await FilePicker.platform.pickFiles( + allowMultiple: false, + type: FileType.custom, + dialogTitle: "Pick a file", + allowedExtensions: [ + ...imgExt, + 'pdf', + 'doc', + 'docx', + 'xls', + 'xlsx', + 'ppt', + 'pptx', + 'txt' + ], + ); + + if (result != null) { + if (result.files.single.size > 10000000) { + SanckbarWidget(context, "File tidak boleh lebih dari 10 MB", + snackbarType.warning); + } else { + // File files = File(result.files.single.path!); + print(result.files.single.extension); + XFile fl = XFile(result.files.single.path!); + fileName.value = result.files.single.name; + + isImage.value = imgExt.contains(result.files.single.extension); + + print(result.files.single.name); + fileData.value = fl; + await getBase64(); + fileEkstension.value = result.files.single.extension ?? ""; + } + } else { + // User canceled the picker + } + } + + Future getAddressFromLocation() async { + try { + isLoadingAddressUserLocation.value = true; + // Mendapatkan posisi pengguna + LocationPermission permission = await Geolocator.requestPermission(); + + if (permission == LocationPermission.denied) { + isLoadingAddressUserLocation.value = false; + SanckbarWidget(context, 'Izin lokasi ditolak', snackbarType.error); + // Handle jika pengguna menolak izin lokasi + print("Izin lokasi ditolak"); + return; + } + + Position position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high); + + // Mendapatkan alamat dari posisi + List placemarks = await placemarkFromCoordinates( + position.latitude, position.longitude); + + if (positionLongitude.value.isEmpty && positionLatitude.value.isEmpty) { + if (placemarks.isNotEmpty) { + isLoadingAddressUserLocation.value = false; + Placemark placemark = placemarks.first; + // String address = + // "${placemark.thoroughfare}, ${placemark.locality}, ${placemark.administrativeArea}, ${placemark.country},"; + + String address = + "${placemark.street}, ${placemark.subLocality}, ${placemark.subAdministrativeArea}, ${placemark.postalCode}"; + print("Alamat: $address"); + + positionLatitude.value = position.latitude.toString(); + positionLongitude.value = position.longitude.toString(); + + if (address != "") { + currentAddressUserLocation.value = address; + } + } else { + isLoadingAddressUserLocation.value = false; + SanckbarWidget( + context, 'Tidak dapat menemukan alamat.', snackbarType.error); + print("Tidak dapat menemukan alamat."); + } + } else { + // jika sudah dapat latitude dan logitude baru panggil check distance provider + if (positionLatitude.value.isNotEmpty && + positionLongitude.value.isNotEmpty) { + print('check distance provider'); + + // panggil check distance provider + ref.read(checkDistanceProvider.notifier).checkDistance( + selectedUser?.model.staffId ?? "", + selectedUser?.model.companyId ?? "", + positionLatitude.value, + positionLongitude.value, + ); + } + } + } catch (e) { + print("Error: $e"); + isLoadingAddressUserLocation.value = false; + SanckbarWidget(context, 'Error : $e', snackbarType.error); + } + } + + Future requestLocationPermission() async { + var status = await Permission.location.request(); + isLoadingAddressUserLocation.value = true; + if (status.isGranted) { + // Izin diberikan, lanjutkan dengan mendapatkan lokasi + getAddressFromLocation(); + } else { + isLoadingAddressUserLocation.value = false; + // Izin ditolak, berikan pemberitahuan atau instruksi + // print('Izin lokasi ditolak'); + SanckbarWidget(context, 'Izin Ditolak', snackbarType.error); + } + } + + // check distance provider + ref.listen(checkDistanceProvider, (prev, next) { + print('status check distance ' + next.toString()); + if (next is CheckDistanceStateLoading) { + isLoadingProsesCheckDistance.value = true; + } else if (next is CheckDistanceStateError) { + isLoadingProsesCheckDistance.value = false; + SanckbarWidget(context, next.message, snackbarType.warning); + } else if (next is CheckDistanceStateDone) { + isLoadingProsesCheckDistance.value = false; + + // tTransactionCurrentDistance.value = + // varCurrentDistanceProvider?.currentDistance ?? ""; + + // print("distance XXX : "+tTransactionCurrentDistance.value); + + if (varCurrentDistanceProvider?.selfie == "FALSE") { + Navigator.pushNamed(context, presensiRoute); + } + } + }); + + // check jam presensi + ref.listen(checkPresensiJamProvider, (prev, next) { + if (next is CheckPresensiJamStateLoading) { + isLoadingProsesCheckDistance.value = true; + } else if (next is CheckPresensiJamStateError) { + isLoadingProsesCheckDistance.value = false; + SanckbarWidget( + context, "Error : " + next.toString(), snackbarType.warning); + } else if (next is CheckPresensiJamStateDone) { + isLoadingProsesCheckDistance.value = false; + } + }); + + // proses presensi clock in + ref.listen(presensiClockInProvider, (prev, next) { + if (next is PresensiClockInStateLoading) { + isLoadingProsesCheckDistance.value = true; + } else if (next is PresensiClockInStateError) { + isLoadingProsesCheckDistance.value = false; + SanckbarWidget( + context, "Error : " + next.toString(), snackbarType.warning); + } else if (next is PresensiClockInStateDone) { + isLoadingProsesCheckDistance.value = false; + if (next.model == "OK") { + SanckbarWidget(context, "Berhasil Absen Masuk", snackbarType.success); + requestLocationPermission(); + } + // else{ + // if(next.model == "WARNING"){ + // SanckbarWidget(context, "Berhasil Absen Masuk", snackbarType.success); + // } + // } + } + }); + + // proses presensi clock in + ref.listen(presensiClockOutProvider, (prev, next) { + if (next is PresensiClockOutStateLoading) { + isLoadingProsesCheckDistance.value = true; + } else if (next is PresensiClockOutStateError) { + isLoadingProsesCheckDistance.value = false; + SanckbarWidget( + context, "Error : " + next.toString(), snackbarType.warning); + } else if (next is PresensiClockOutStateDone) { + isLoadingProsesCheckDistance.value = false; + if (next.model == "OK") { + SanckbarWidget( + context, "Berhasil Absen Pulang", snackbarType.success); + requestLocationPermission(); + } + // else{ + // if(next.model == "WARNING"){ + // SanckbarWidget(context, "Berhasil Absen Masuk", snackbarType.success); + // } + // } + } + }); + + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + final staffID = ref.read(currentUserProvider)?.model.staffId ?? "0"; + if (staffID == "0") { + //not login + Navigator.of(context) + .pushNamedAndRemoveUntil(loginRoute, (route) => true); + + // Navigator.popAndPushNamed(context, loginRoute); + return; + } + + // requestLocationPermission(); + }); + return () {}; + }, []); + + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + requestLocationPermission(); + }); + return () {}; + }, []); + + // show konfirmasi dialog + Future showConfirmationDialog( + BuildContext context, + String T_TransactionM_StaffID, + String T_TransactionM_CompanyID, + String T_TransactionCurrentLatitude, + String T_TransactionCurrentLongitude, + String T_TransactionCurrentDistance, + String T_TransactionSelfiePhoto, + String token, + String tipeAbsen, + ) async { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('Konfirmasi'), + content: Text('Apakah anda yakin untuk melakukan $tipeAbsen?'), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + actions: [ + ElevatedButton( + onPressed: () { + if (T_TransactionSelfiePhoto == "") { + SanckbarWidget( + context, + "Silahkan Foto Selfie Terlebih Dahulu", + snackbarType.warning); + } else { + Map param = { + "T_TransactionM_StaffID": T_TransactionM_StaffID, + "T_TransactionM_CompanyID": T_TransactionM_CompanyID, + "T_TransactionCurrentLatitude": + T_TransactionCurrentLatitude, + "T_TransactionCurrentLongitude": + T_TransactionCurrentLongitude, + "T_TransactionCurrentDistance": + T_TransactionCurrentDistance, + "T_TransactionSelfiePhoto": "data:image/jpeg;base64,$T_TransactionSelfiePhoto", + "token": token, + "isSelfie": "TRUE" + }; + // print(param); + + if (tipeAbsen == "Clock In") { + ref + .read(presensiClockInProvider.notifier) + .presensiClockIn( + T_TransactionM_StaffID, + T_TransactionM_CompanyID, + T_TransactionCurrentLatitude, + T_TransactionCurrentLongitude, + T_TransactionCurrentDistance, + T_TransactionSelfiePhoto, + token, + "FALSE", + param, + ); + Navigator.of(context).pop(); + } else { + if (tipeAbsen == "Clock Out") { + ref + .read(presensiClockOutProvider.notifier) + .presensiClockOut( + T_TransactionM_StaffID, + T_TransactionM_CompanyID, + T_TransactionCurrentLatitude, + T_TransactionCurrentLongitude, + T_TransactionCurrentDistance, + T_TransactionSelfiePhoto, + token, + "FALSE", + param, + ); + Navigator.of(context).pop(); + } + } + } + }, + child: Text( + 'Yakin', + style: Constant.logintitle_700(context: context).copyWith( + fontWeight: FontWeight.w600, + color: Constant.textWhite, + ), + ), + style: ButtonStyle( + backgroundColor: MaterialStateColor.resolveWith( + (st) => Constant.textOrange), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide( + color: Constant.textOrange, + ), + ), + ), + shadowColor: MaterialStateProperty.all(Color(0xffff48423d)), + elevation: MaterialStateProperty.all(4.0), + ), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); // Tutup dialog + }, + style: ButtonStyle( + backgroundColor: MaterialStateColor.resolveWith( + (st) => Constant.textWhite), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide( + color: Constant.textBlack, + ), + ), + ), + shadowColor: MaterialStateProperty.all(Color(0xffff48423d)), + elevation: MaterialStateProperty.all(4.0), + ), + child: Text( + 'Batal', + style: TextStyle( + color: Constant.textTrueBlack, + ), + ), + ), + ], + ); + }, + ); + } + return Padding( padding: EdgeInsets.only( top: Constant.getActualYPhone(context: context, y: 30), @@ -15,7 +483,7 @@ class PresensiSelfieScreen extends StatelessWidget { appBar: AppBar( title: Text( // 'Home Screen', - 'Presensi', + 'Presensi Selfie', overflow: TextOverflow.ellipsis, style: TextStyle( color: Constant.textBlack, @@ -28,6 +496,7 @@ class PresensiSelfieScreen extends StatelessWidget { // elevation: 1.0, elevation: 0.5, ), + drawer: CustomDrawer(), body: SafeArea( child: Padding( padding: EdgeInsets.only( @@ -37,8 +506,442 @@ class PresensiSelfieScreen extends StatelessWidget { ), child: Container( width: Constant.getActualXPhone(context: context, x: 390), - height: 100, - color: Colors.green, + // height: Constant.getActualYPhone(context: context, y: 844), + height: MediaQuery.of(context).size.height, + child: Column( + children: [ + // Text(ref.watch(currentCheckDistanceProvider)?.currentDistance ?? "NULL"), + // Spacer(), + // tanggal sekarang + RealTimeFormattedDate(), + SizedBox( + height: Constant.getActualYPhone(context: context, y: 8), + ), + // jam sekarang + RealTimeClock(), + SizedBox( + height: Constant.getActualYPhone(context: context, y: 24), + ), + + // address dan refresh + Container( + width: Constant.getActualXPhone(context: context, x: 350), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Constant.bgAddressPresensi, + ), + child: Padding( + padding: EdgeInsets.only( + left: Constant.getActualXPhone(context: context, x: 12), + right: + Constant.getActualXPhone(context: context, x: 12), + top: Constant.getActualYPhone(context: context, y: 12), + bottom: + Constant.getActualYPhone(context: context, y: 12), + ), + child: Row( + children: [ + // Bagian kiri + Padding( + padding: EdgeInsets.only(left: 12, right: 8), + child: Column( + children: [ + Icon( + Icons.location_on_outlined, + color: Constant.textDarkGrey, + ), + ], + ), + ), + + // Bagian tengah + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Column( + children: [ + (isLoadingAddressUserLocation.value) + ? Center( + child: CircularProgressIndicator(), + ) + : Text( + // 'Perumahan Grand House of Klodran No. 5, Klodran, Kec. Colomadu, Kabupaten Karanganyar, Jawa Tengah 57172', + currentAddressUserLocation.value, + overflow: TextOverflow.ellipsis, + maxLines: 10, + style: Constant.titleH2_400_12( + context: context) + .copyWith( + color: Constant.textDarkGrey, + ), + ), + ], + ), + ), + ), + + // Bagian kanan + Padding( + padding: EdgeInsets.only(right: 12), + child: Column( + children: [ + InkWell( + onTap: () async { + getAddressFromLocation(); + // jika sudah dapat latitude dan logitude baru panggil check distance provider + if (positionLatitude.value.isNotEmpty && + positionLongitude.value.isNotEmpty) { + // panggil check distance provider + ref + .read(checkDistanceProvider.notifier) + .checkDistance( + selectedUser?.model.staffId ?? "", + selectedUser?.model.companyId ?? "", + positionLatitude.value, + positionLongitude.value, + ); + } + }, + child: Container( + width: Constant.getActualXPhone( + context: context, x: 36), + height: Constant.getActualYPhone( + context: context, y: 36), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Constant.textOrange, + ), + child: Image.asset( + 'images/sync_white.png', // Path gambar untuk "Refresh" + width: Constant.getActualXPhone( + context: context, x: 20), + height: Constant.getActualYPhone( + context: context, y: 20), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + + SizedBox( + height: Constant.getActualYPhone(context: context, y: 6), + ), + + Padding( + padding: EdgeInsets.only( + left: + Constant.getActualXPhone(context: context, x: 12)), + child: Row( + children: [ + Container( + width: + Constant.getActualXPhone(context: context, x: 16), + height: + Constant.getActualYPhone(context: context, y: 16), + decoration: BoxDecoration( + image: DecorationImage( + // fit: BoxFit.cover, + image: AssetImage( + 'images/warning_selfie.png'), // Ganti dengan path gambar Anda + ), + ), + ), + SizedBox( + width: + Constant.getActualXPhone(context: context, x: 4), + ), + Text( + 'Anda berada diluar kantor, silahkan ajukan approval ', + style: + Constant.titleH2_400(context: context).copyWith( + color: Constant.textRed, + ), + ), + ], + ), + ), + + SizedBox( + height: Constant.getActualYPhone(context: context, y: 28), + ), + + // upload file + if (varCurrentCheckJamProvider?.isAbsenClockIn == "TRUE" && + varCurrentCheckJamProvider?.isAbsenClockOut == "TRUE") ...[ + SizedBox.shrink() + ] else ...[ + if (varCurrentCheckJamProvider?.isAbsenClockIn == "FALSE" && + varCurrentCheckJamProvider?.jamClockIn == "") ...[ + // gambar icon presensi clock in + (isLoadingAddressUserLocation.value) + ? Center( + child: CircularProgressIndicator(), + ) + : PresensiSelfieUploadAreaWidget( + isLoading: isLoadingAddressUserLocation, + isImage: isImage, + fileData: fileData, + fileDataBase64: fileDataBase64, + pickFile: pickFile, + pickImage: pickImage, + ), + ] else ...[ + // gambar icon presensi clock out + if (varCurrentCheckJamProvider?.isAbsenClockIn == + "TRUE" && + varCurrentCheckJamProvider?.jamClockIn != "") ...[ + (isLoadingAddressUserLocation.value) + ? Center( + child: CircularProgressIndicator(), + ) + : PresensiSelfieUploadAreaWidget( + isLoading: isLoadingAddressUserLocation, + isImage: isImage, + fileData: fileData, + fileDataBase64: fileDataBase64, + pickFile: pickFile, + pickImage: pickImage, + ), + ] + ], + ], + + Spacer(), + + // button clock in dan clock out + if (varCurrentCheckJamProvider?.isAbsenClockIn == "TRUE" && + varCurrentCheckJamProvider?.isAbsenClockOut == "TRUE") ...[ + SizedBox.shrink() + ] else ...[ + if (varCurrentCheckJamProvider?.isAbsenClockIn == "FALSE" && + varCurrentCheckJamProvider?.jamClockIn == "") ...[ + // gambar icon presensi clock in + (isLoadingAddressUserLocation.value) + ? Center( + child: CircularProgressIndicator(), + ) + : Padding( + padding: EdgeInsets.only( + left: Constant.getActualXPhone( + context: context, x: 20), + right: Constant.getActualXPhone( + context: context, x: 20), + top: Constant.getActualYPhone( + context: context, y: 24), + bottom: Constant.getActualYPhone( + context: context, y: 24), + ), + child: SizedBox( + width: Constant.getActualXPhone( + context: context, x: 390), + // height: + // Constant.getActualYPhone(context: context, y: 50), + child: ElevatedButton( + // onPressed: () { + // Navigator.of(context).pushNamed(homeRoute); + // }, + onPressed: () { + final T_TransactionM_StaffID = + selectedUser?.model.staffId ?? ""; + final T_TransactionM_CompanyID = + selectedUser?.model.companyId ?? ""; + final T_TransactionCurrentLatitude = + positionLatitude.value; + final T_TransactionCurrentLongitude = + positionLongitude.value; + final T_TransactionCurrentDistance = + varCurrentDistanceProvider + ?.currentDistance ?? + ""; + final T_TransactionSelfiePhoto = + fileDataBase64.value; + final token = selectedUser?.token ?? ""; + + showConfirmationDialog( + context, + T_TransactionM_StaffID, + T_TransactionM_CompanyID, + T_TransactionCurrentLatitude, + T_TransactionCurrentLongitude, + T_TransactionCurrentDistance, + T_TransactionSelfiePhoto, + token, + "Clock In"); + + // print( + // fileDataBase64.value, + // ); + }, + style: ButtonStyle( + backgroundColor: + MaterialStateColor.resolveWith( + (st) => Constant.textOrange), + shape: MaterialStateProperty.all< + RoundedRectangleBorder>( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide( + color: Constant.textOrange, + ), + ), + ), + shadowColor: MaterialStateProperty.all( + Color(0xffff48423d)), + elevation: MaterialStateProperty.all(4.0), + ), + child: Stack( + children: [ + (isLoadingAddressUserLocation.value) + ? SizedBox( + width: Constant.getActualXPhone( + context: context, x: 24), + height: Constant.getActualYPhone( + context: context, y: 32), + child: Center( + child: + CircularProgressIndicator( + color: Constant.textOrange, + ), + ), + ) + : Align( + alignment: Alignment.center, + child: Text( + 'Clock In', + style: Constant.titleH1_500_18( + context: context) + .copyWith( + color: Constant.textWhite, + ), + ), + ), + ], + ), + ), + ), + ), + ] else ...[ + // gambar icon presensi clock out + if (varCurrentCheckJamProvider?.isAbsenClockIn == + "TRUE" && + varCurrentCheckJamProvider?.jamClockIn != "") ...[ + (isLoadingAddressUserLocation.value) + ? Center( + child: CircularProgressIndicator(), + ) + : Padding( + padding: EdgeInsets.only( + left: Constant.getActualXPhone( + context: context, x: 20), + right: Constant.getActualXPhone( + context: context, x: 20), + top: Constant.getActualYPhone( + context: context, y: 24), + bottom: Constant.getActualYPhone( + context: context, y: 24), + ), + child: SizedBox( + width: Constant.getActualXPhone( + context: context, x: 390), + // height: + // Constant.getActualYPhone(context: context, y: 50), + child: ElevatedButton( + // onPressed: () { + // Navigator.of(context).pushNamed(homeRoute); + // }, + onPressed: () { + final T_TransactionM_StaffID = + selectedUser?.model.staffId ?? ""; + final T_TransactionM_CompanyID = + selectedUser?.model.companyId ?? ""; + final T_TransactionCurrentLatitude = + positionLatitude.value; + final T_TransactionCurrentLongitude = + positionLongitude.value; + final T_TransactionCurrentDistance = + varCurrentDistanceProvider + ?.currentDistance ?? + ""; + final T_TransactionSelfiePhoto = + fileDataBase64.value; + final token = selectedUser?.token ?? ""; + + showConfirmationDialog( + context, + T_TransactionM_StaffID, + T_TransactionM_CompanyID, + T_TransactionCurrentLatitude, + T_TransactionCurrentLongitude, + T_TransactionCurrentDistance, + T_TransactionSelfiePhoto, + token, + "Clock Out"); + // print( + // fileDataBase64.value, + // ); + }, + style: ButtonStyle( + backgroundColor: + MaterialStateColor.resolveWith( + (st) => Constant.textOrange), + shape: MaterialStateProperty.all< + RoundedRectangleBorder>( + RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(8), + side: BorderSide( + color: Constant.textOrange, + ), + ), + ), + shadowColor: MaterialStateProperty.all( + Color(0xffff48423d)), + elevation: MaterialStateProperty.all(4.0), + ), + child: Stack( + children: [ + (isLoadingAddressUserLocation.value) + ? SizedBox( + width: Constant.getActualXPhone( + context: context, x: 24), + height: + Constant.getActualYPhone( + context: context, + y: 32), + child: Center( + child: + CircularProgressIndicator( + color: Constant.textOrange, + ), + ), + ) + : Align( + alignment: Alignment.center, + child: Text( + 'Clock Out', + style: + Constant.titleH1_500_18( + context: context) + .copyWith( + color: Constant.textWhite, + ), + ), + ), + ], + ), + ), + ), + ), + ] + ], + ], + ], + ), ), ), ), diff --git a/lib/screen/presensi/presensi_selfie_upload_area.dart b/lib/screen/presensi/presensi_selfie_upload_area.dart new file mode 100644 index 0000000..72c7286 --- /dev/null +++ b/lib/screen/presensi/presensi_selfie_upload_area.dart @@ -0,0 +1,181 @@ +import 'dart:io'; + +import '../../app/constant.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:mime/mime.dart'; + +class PresensiSelfieUploadAreaWidget extends StatelessWidget { + const PresensiSelfieUploadAreaWidget({ + super.key, + required this.isImage, + required this.fileData, + required this.fileDataBase64, + required this.pickFile, + required this.pickImage, + required this.isLoading, + }); + + final ValueNotifier isImage; + final ValueNotifier isLoading; + final ValueNotifier fileData; + final ValueNotifier fileDataBase64; + final Function pickImage; + final Function pickFile; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: !isLoading.value + ? () { + showModalBottomSheet( + context: context, + builder: (context) { + return Container( + height: Constant.getActualYPhone(context: context, y: 200), + padding: EdgeInsets.all(20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Column( + // children: [ + // IconButton( + // onPressed: () { + // Navigator.pop(context); + // pickFile(); + // }, + // icon: Icon( + // Icons.folder_copy_rounded, + // size: 50, + // color: Constant.textOrange, + // )), + // Text( + // "Browse a file", + // style: Constant.titleH2_400_12(context: context) + // .copyWith( + // fontWeight: FontWeight.w600, + // color: Constant.textBlack, + // ), + // ) + // ], + // ), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + Navigator.pop(context); + pickImage(); + }, + icon: Icon( + Icons.add_a_photo_rounded, + size: 50, + color: Constant.textOrange, + ), + ), + SizedBox( + height: Constant.getActualYPhone( + context: context, y: 10), + ), + Text( + "Take a picture", + style: Constant.titleH2_400_12(context: context) + .copyWith( + fontWeight: FontWeight.w600, + color: Constant.textBlack, + ), + ) + ], + ), + ], + ), + ); + }, + ); + } + : null, + child: Stack( + alignment: AlignmentDirectional.topEnd, + children: [ + Container( + width: Constant.getActualXPhone(context: context, x: 390), + height: Constant.getActualYPhone( + context: context, y: isImage.value ? 200 : 83), + decoration: BoxDecoration(color: Constant.bgUploadFile), + child: Builder(builder: (context) { + final String? mime = lookupMimeType(fileData.value?.path ?? ""); + return Semantics( + label: 'image_picker_example_picked_image', + child: (mime != null + ? (mime.startsWith('image/')) + ? Image.file( + // image: AssetImage(photo.value!.path), + File(fileData.value!.path), + frameBuilder: (context, child, frame, + wasSynchronouslyLoaded) { + return (wasSynchronouslyLoaded) + ? Center( + child: Text("Loadinga"), + ) + : Container( + child: child, + ); + }, + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: Text( + 'This image type is not supported')); + }, + ) + : Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.file_present_rounded, + size: 40, + color: Constant.textOrange, + ), + Text( + fileData.value?.name ?? '', + style: + Constant.titleH2_400(context: context), + ) + ], + ), + ) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + Icons.upload_outlined, + color: Constant.textOrange, + ), + Text( + 'Upload File', + style: Constant.titleH2_400_12(context: context) + .copyWith( + fontWeight: FontWeight.w600, + color: Constant.textOrange), + ) + ], + ))); + }), + ), + if (fileData.value != null && !isLoading.value) + IconButton( + onPressed: () { + fileData.value = null; + fileDataBase64.value = ''; + isImage.value = false; + }, + icon: Icon(Icons.cancel_outlined)), + ], + ), + ); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..64a0ece 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..2db3c22 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 738f6dd..53d8e6c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,11 +5,13 @@ import FlutterMacOS import Foundation +import file_selector_macos import geolocator_apple import path_provider_foundation import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 732bac8..0962271 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "2f9d2cbccb76127ba28528cb3ae2c2326a122446a83de5a056aaa3880d3882c5" + url: "https://pub.dev" + source: hosted + version: "0.3.3+7" crypto: dependency: transitive description: @@ -97,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" + csslib: + dependency: transitive + description: + name: csslib + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + url: "https://pub.dev" + source: hosted + version: "1.0.0" cupertino_icons: dependency: "direct main" description: @@ -145,6 +161,46 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "4e42aacde3b993c5947467ab640882c56947d9d27342a5b6f2895b23956954a6" + url: "https://pub.dev" + source: hosted + version: "6.1.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" flutter: dependency: "direct main" description: flutter @@ -174,6 +230,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + url: "https://pub.dev" + source: hosted + version: "2.0.17" flutter_riverpod: dependency: "direct main" description: @@ -408,6 +472,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + html: + dependency: transitive + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" + source: hosted + version: "0.15.4" http: dependency: transitive description: @@ -424,6 +496,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd" + url: "https://pub.dev" + source: hosted + version: "1.0.7" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" + url: "https://pub.dev" + source: hosted + version: "0.8.9+3" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3 + url: "https://pub.dev" + source: hosted + version: "3.0.2" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 + url: "https://pub.dev" + source: hosted + version: "0.8.9+1" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b + url: "https://pub.dev" + source: hosted + version: "2.9.3" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" intl: dependency: "direct main" description: @@ -504,6 +640,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mime: + dependency: "direct main" + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + mobkit_dashed_border: + dependency: "direct main" + description: + name: mobkit_dashed_border + sha256: "6ebfcf7ff3ea8aed883dc08fae03a3a8a5a33c21b4dc4dfac89081426394cc62" + url: "https://pub.dev" + source: hosted + version: "0.0.5" normalize: dependency: transitive description: @@ -512,6 +664,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.8.2+1" + open_file: + dependency: "direct main" + description: + name: open_file + sha256: a5a32d44acb7c899987d0999e1e3cbb0a0f1adebbf41ac813ec6d2d8faa0af20 + url: "https://pub.dev" + source: hosted + version: "3.3.2" package_config: dependency: transitive description: @@ -616,6 +776,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.3" + photo_view: + dependency: "direct main" + description: + name: photo_view + sha256: "8036802a00bae2a78fc197af8a158e3e2f7b500561ed23b4c458107685e645bb" + url: "https://pub.dev" + source: hosted + version: "0.14.0" pigeon: dependency: transitive description: @@ -853,6 +1021,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: "74b86e63529cf5885130c639d74cd2f9232e7c8a66cbecbddd1dcb9dbd060d1e" + url: "https://pub.dev" + source: hosted + version: "2.7.2" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: "7f8f25d7ad56819a82b2948357f3c3af071f6a678db33833b26ec36bbc221316" + url: "https://pub.dev" + source: hosted + version: "2.4.11" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: bf1a1322bf68bccd349982ba1f5a41314a3880861fb9a93d25d6d0a2345845f0 + url: "https://pub.dev" + source: hosted + version: "2.4.11" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" + url: "https://pub.dev" + source: hosted + version: "6.2.2" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "9c34a243785feca23148bfcd772dbb803d63c9304488177ec4f3f4463802fcb7" + url: "https://pub.dev" + source: hosted + version: "2.0.17" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e334ccb..20c0f79 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,13 @@ dependencies: graphql: ^5.1.3 top_snackbar_flutter: ^3.1.0 permission_handler: ^11.0.0 + mobkit_dashed_border: ^0.0.5 + file_picker: ^6.1.1 + open_file: ^3.3.2 + image_picker: ^1.0.7 + video_player: ^2.7.2 + photo_view: ^0.14.0 + mime: ^1.0.4 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 5be7b60..921279f 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,10 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); GeolocatorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("GeolocatorWindows")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b949ced..71dd257 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows geolocator_windows permission_handler_windows )