From 900cf996813870975728b457c42ce832bc9986ef Mon Sep 17 00:00:00 2001 From: 202310715280-FADLAN-RIVALDI <202310715280@mhs.ubharajaya.ac.id> Date: Tue, 13 Jan 2026 22:31:09 +0700 Subject: [PATCH] Update Readme --- README.md | 61 ++-- app/build.gradle.kts | 17 ++ .../ubharajaya/sistemakademik/MainActivity.kt | 280 +----------------- .../sistemakademik/adapter/HistoryAdapter.kt | 56 ++++ .../sistemakademik/data/UserProfile.kt | 20 ++ .../ui/fragments/AbsensiFragment.kt | 191 ++++++++++++ .../ui/fragments/HistoryFragment.kt | 55 ++++ .../ui/fragments/PreviewFragment.kt | 171 +++++++++++ .../ui/fragments/ProfileFragment.kt | 65 ++++ app/src/main/res/layout/activity_main.xml | 29 +- app/src/main/res/layout/fragment_absensi.xml | 53 +++- app/src/main/res/layout/fragment_history.xml | 34 ++- app/src/main/res/layout/fragment_preview.xml | 178 ++++++++++- app/src/main/res/layout/fragment_profile.xml | 80 ++++- app/src/main/res/layout/item_history.xml | 61 +++- app/src/main/res/menu/bottom_nav_menu.xml | 13 +- app/src/main/res/navigation/nav_graph.xml | 29 +- app/src/main/res/values-night/themes.xml | 18 +- app/src/main/res/values/themes.xml | 19 +- 19 files changed, 1108 insertions(+), 322 deletions(-) diff --git a/README.md b/README.md index 9871f13..fb2e62e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -# ๐Ÿ“ฑ Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile) +# ๐Ÿ“ฑ Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile)## ๐Ÿ‘ค Identitas Mahasiswa +- **Nama:** Fadlan Rivaldi +- **NPM:** 202310715280 + +--- ## ๐Ÿ“Œ Deskripsi Proyek Proyek ini merupakan **Tugas Project Akhir Mata Kuliah Pemrograman Mobile** yang bertujuan untuk membangun **aplikasi akademik berbasis mobile** dengan fokus pada **fitur absensi menggunakan data koordinat (GPS) dan pengambilan foto mahasiswa**. @@ -50,48 +54,49 @@ Aplikasi ini dirancang untuk meningkatkan **validitas kehadiran mahasiswa**, den - Foto diambil menggunakan **kamera depan (selfie)** - Foto hanya dapat diambil **saat proses absensi** - Foto disimpan sebagai **bukti kehadiran** -- Foto dapat digunakan untuk: - - Verifikasi manual oleh dosen - - Dokumentasi akademik +- Foto dapat digunakan untuk verifikasi manual dan dokumentasi akademik. --- ## ๐Ÿ› ๏ธ Teknologi yang Digunakan -- **Platform** : Android -- **Bahasa Pemrograman** : Kotlin / Java -- **Location Service** : - - Google Maps API - - Fused Location Provider -- **Camera API** : CameraX / Camera2 -- **Database** : Firebase / SQLite / MySQL -- **Storage** : Firebase Storage / Local Storage -- **IDE** : Android Studio +- **Platform**: Android +- **Bahasa Pemrograman**: Kotlin +- **Location Service**: Google Maps API / Fused Location Provider +- **Camera API**: CameraX +- **Architecture**: MVVM / View System & Jetpack Compose +- **IDE**: Android Studio --- ## ๐Ÿ” Izin Aplikasi (Permissions) Aplikasi memerlukan izin berikut: -- `ACCESS_FINE_LOCATION` -- `ACCESS_COARSE_LOCATION` +- `ACCESS_FINE_LOCATION` & `ACCESS_COARSE_LOCATION` - `CAMERA` - `INTERNET` -- `WRITE_EXTERNAL_STORAGE` (jika diperlukan) --- ## ๐Ÿ“‚ Mockup ![mockup](Mockup.png) -gambar mockup dibuat oleh AI +*Gambar mockup dibuat oleh AI* -## Catatan: -- Starter project ini dibuat berbantukan AI -- Kembangkan project dari starter yang sudah disediakan, jangan membuat dari awal. -- Untuk koordinat bisa ditambah/kurangi angka tertentu agar tidak memunculkan koordinat rumah masing-masing, data awal tetap dari GPS. - -## Pengecekan: -- https://ntfy.ubharajaya.ac.id/EAS -- https://docs.google.com/spreadsheets/d/1jH15MfnNgpPGuGeid0hYfY7fFUHCEFbCmg8afTyyLZs/edit?gid=0#gid=0 +--- -## Webhook: -- test: https://n8n.lab.ubharajaya.ac.id/webhook-test/23c6993d-1792-48fb-ad1c-ffc78a3e6254 -- production: https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254 \ No newline at end of file +## โš ๏ธ Disclaimer & Catatan Penting +- **PENGGUNAAN AI**: Proyek ini dikembangkan dengan bantuan **Kecerdasan Buatan (AI)** dalam proses debugging, pembuatan starter project, dan penyusunan dokumentasi. +- **PENGEMBANGAN**: Proyek ini dikembangkan dari *starter project* yang disediakan dan tidak dibuat dari nol. +- **PRIVASI KOORDINAT**: Untuk alasan keamanan/privasi, angka koordinat GPS dapat dimodifikasi sedikit agar tidak menunjukkan lokasi rumah pribadi secara presisi. + +--- + +## ๐Ÿ”— Link Pengecekan & Webhook +- **Monitoring**: [ntfy.ubharajaya.ac.id/EAS](https://ntfy.ubharajaya.ac.id/EAS) +- **Data Spreadsheet**: [Google Sheets](https://docs.google.com/spreadsheets/d/1jH15MfnNgpPGuGeid0hYfY7fFUHCEFbCmg8afTyyLZs/edit?gid=0#gid=0) +- **Webhook Production**: `https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254` + +--- +โ€œโš ๏ธ Disclaimer & Catatan Pentingโ€ +Proyek ini dikembangkan dengan bantuan Kecerdasan Buatan (AI) sebagai asisten dalam proses debugging dan dokumentasi. Seluruh implementasi, pemahaman konsep, +dan pengembangan fitur dilakukan oleh penulis secara mandiri. + +*Dibuat untuk memenuhi Tugas Project Akhir EAS 2025/2026.* \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7d76378..2652f6b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -51,8 +51,25 @@ dependencies { implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) + implementation ("com.google.android.material:material:1.11.0") + + // Location (GPS) implementation("com.google.android.gms:play-services-location:21.0.1") + // Navigation Component + implementation("androidx.navigation:navigation-fragment-ktx:2.7.6") + implementation("androidx.navigation:navigation-ui-ktx:2.7.6") + + // ViewModel & LiveData + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") + + // Google Play Services Location + implementation("com.google.android.gms:play-services-location:21.0.1") + + // Gson untuk parsing JSON + implementation("com.google.code.gson:gson:2.10.1") + testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt index c774502..b2a81d0 100644 --- a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt @@ -1,274 +1,24 @@ package id.ac.ubharajaya.sistemakademik -import android.Manifest -import android.app.Activity -import android.content.Intent -import android.content.pm.PackageManager -import android.graphics.Bitmap import android.os.Bundle -import android.provider.MediaStore -import android.util.Base64 -import android.widget.Toast -import androidx.activity.ComponentActivity -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat -import com.google.android.gms.location.LocationServices -import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme -import org.json.JSONObject -import java.io.ByteArrayOutputStream -import java.net.HttpURLConnection -import java.net.URL -import kotlin.concurrent.thread +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.setupWithNavController +import com.google.android.material.bottomnavigation.BottomNavigationView -/* ================= UTIL ================= */ - -fun bitmapToBase64(bitmap: Bitmap): String { - val outputStream = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream) - return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP) -} - -fun kirimKeN8n( - context: ComponentActivity, - latitude: Double, - longitude: Double, - foto: Bitmap -) { - thread { - try { - val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254") -// test URL val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook-test/23c6993d-1792-48fb-ad1c-ffc78a3e6254") - val conn = url.openConnection() as HttpURLConnection - - conn.requestMethod = "POST" - conn.setRequestProperty("Content-Type", "application/json") - conn.doOutput = true - - val json = JSONObject().apply { - put("npm", "12345") - put("nama","Arif R D") - put("latitude", latitude) - put("longitude", longitude) - put("timestamp", System.currentTimeMillis()) - put("foto_base64", bitmapToBase64(foto)) - } - - conn.outputStream.use { - it.write(json.toString().toByteArray()) - } - - val responseCode = conn.responseCode - - context.runOnUiThread { - Toast.makeText( - context, - if (responseCode == 200) - "Absensi diterima server" - else - "Absensi ditolak server", - Toast.LENGTH_SHORT - ).show() - } - - conn.disconnect() - - } catch (_: Exception) { - context.runOnUiThread { - Toast.makeText( - context, - "Gagal kirim ke server", - Toast.LENGTH_SHORT - ).show() - } - } - } -} - -/* ================= ACTIVITY ================= */ - -class MainActivity : ComponentActivity() { +class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - enableEdgeToEdge() + setContentView(R.layout.activity_main) - setContent { - SistemAkademikTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - AbsensiScreen( - modifier = Modifier.padding(innerPadding), - activity = this - ) - } - } - } + // Setup Navigation + val navHostFragment = supportFragmentManager + .findFragmentById(R.id.nav_host_fragment) as NavHostFragment + val navController = navHostFragment.navController + + // Setup Bottom Navigation + val bottomNav = findViewById(R.id.bottom_navigation) + bottomNav.setupWithNavController(navController) } -} - -/* ================= UI ================= */ - -@Composable -fun AbsensiScreen( - modifier: Modifier = Modifier, - activity: ComponentActivity -) { - val context = LocalContext.current - - var lokasi by remember { mutableStateOf("Koordinat: -") } - var latitude by remember { mutableStateOf(null) } - var longitude by remember { mutableStateOf(null) } - var foto by remember { mutableStateOf(null) } - - val fusedLocationClient = - LocationServices.getFusedLocationProviderClient(context) - - /* ===== Permission Lokasi ===== */ - - val locationPermissionLauncher = - rememberLauncherForActivityResult( - ActivityResultContracts.RequestPermission() - ) { granted -> - if (granted) { - - if ( - ContextCompat.checkSelfPermission( - context, - Manifest.permission.ACCESS_FINE_LOCATION - ) == PackageManager.PERMISSION_GRANTED - ) { - - fusedLocationClient.lastLocation - .addOnSuccessListener { location -> - if (location != null) { - latitude = location.latitude - longitude = location.longitude - lokasi = - "Lat: ${location.latitude}\nLon: ${location.longitude}" - } else { - lokasi = "Lokasi tidak tersedia" - } - } - .addOnFailureListener { - lokasi = "Gagal mengambil lokasi" - } - } - - } else { - Toast.makeText( - context, - "Izin lokasi ditolak", - Toast.LENGTH_SHORT - ).show() - } - } - - /* ===== Kamera ===== */ - - val cameraLauncher = - rememberLauncherForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == Activity.RESULT_OK) { - val bitmap = - result.data?.extras?.getParcelable("data", Bitmap::class.java) - if (bitmap != null) { - foto = bitmap - Toast.makeText( - context, - "Foto berhasil diambil", - Toast.LENGTH_SHORT - ).show() - } - } - } - - val cameraPermissionLauncher = - rememberLauncherForActivityResult( - ActivityResultContracts.RequestPermission() - ) { granted -> - if (granted) { - val intent = - Intent(MediaStore.ACTION_IMAGE_CAPTURE) - cameraLauncher.launch(intent) - } else { - Toast.makeText( - context, - "Izin kamera ditolak", - Toast.LENGTH_SHORT - ).show() - } - } - - /* ===== Request Awal ===== */ - - LaunchedEffect(Unit) { - locationPermissionLauncher.launch( - Manifest.permission.ACCESS_FINE_LOCATION - ) - } - - /* ===== UI ===== */ - - Column( - modifier = modifier - .fillMaxSize() - .padding(24.dp), - verticalArrangement = Arrangement.Center - ) { - - Text( - text = "Absensi Akademik", - style = MaterialTheme.typography.titleLarge - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text(text = lokasi) - - Spacer(modifier = Modifier.height(16.dp)) - - Button( - onClick = { - cameraPermissionLauncher.launch( - Manifest.permission.CAMERA - ) - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Ambil Foto") - } - - Spacer(modifier = Modifier.height(12.dp)) - - Button( - onClick = { - if (latitude != null && longitude != null && foto != null) { - kirimKeN8n( - activity, - latitude!!, - longitude!!, - foto!! - ) - } else { - Toast.makeText( - context, - "Lokasi atau foto belum lengkap", - Toast.LENGTH_SHORT - ).show() - } - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Kirim Absensi") - } - } -} +} \ No newline at end of file diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/adapter/HistoryAdapter.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/adapter/HistoryAdapter.kt index 974f320..3fa5bba 100644 --- a/app/src/main/java/id/ac/ubharajaya/sistemakademik/adapter/HistoryAdapter.kt +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/adapter/HistoryAdapter.kt @@ -1,2 +1,58 @@ package id.ac.ubharajaya.sistemakademik.adapter +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Base64 +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import id.ac.ubharajaya.sistemakademik.R +import id.ac.ubharajaya.sistemakademik.data.AbsensiData +import java.text.SimpleDateFormat +import java.util.* + +class HistoryAdapter( + private val dataList: List +) : RecyclerView.Adapter() { + + class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val ivThumbnail: ImageView = view.findViewById(R.id.ivThumbnail) + val tvNama: TextView = view.findViewById(R.id.tvHistoryNama) + val tvTanggal: TextView = view.findViewById(R.id.tvHistoryTanggal) + val tvLokasi: TextView = view.findViewById(R.id.tvHistoryLokasi) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_history, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val data = dataList[position] + + // Set foto thumbnail + val bitmap = base64ToBitmap(data.fotoBase64) + holder.ivThumbnail.setImageBitmap(bitmap) + + // Set nama + holder.tvNama.text = data.nama + + // Set tanggal + val dateFormat = SimpleDateFormat("dd MMM yyyy, HH:mm", Locale("id", "ID")) + holder.tvTanggal.text = "๐Ÿ“… ${dateFormat.format(Date(data.timestamp))}" + + // Set lokasi + holder.tvLokasi.text = "๐Ÿ“ Lat: ${data.latitude}, Lon: ${data.longitude}" + } + + override fun getItemCount() = dataList.size + + private fun base64ToBitmap(base64: String): Bitmap { + val decodedBytes = Base64.decode(base64, Base64.NO_WRAP) + return BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size) + } +} diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/data/UserProfile.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/data/UserProfile.kt index dd00b9c..ac88714 100644 --- a/app/src/main/java/id/ac/ubharajaya/sistemakademik/data/UserProfile.kt +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/data/UserProfile.kt @@ -1,2 +1,22 @@ package id.ac.ubharajaya.sistemakademik.data +data class UserProfile( + var nama: String = "", + var npm: String = "" +) +data class AbsensiData( + val nama: String, + val npm: String, + val latitude: Double, + val longitude: Double, + val timestamp: Long, + val fotoBase64: String, + val alamat: String = "Alamat tidak tersedia" +) + +// Singleton untuk menyimpan data sementara +object DataHolder { + var userProfile: UserProfile = UserProfile() + var currentAbsensi: AbsensiData? = null + val historyList = mutableListOf() +} diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/fragments/AbsensiFragment.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/fragments/AbsensiFragment.kt index e99dc5b..af8bbb3 100644 --- a/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/fragments/AbsensiFragment.kt +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/fragments/AbsensiFragment.kt @@ -1,2 +1,193 @@ package id.ac.ubharajaya.sistemakademik.ui.fragments +import android.Manifest +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.os.Bundle +import android.provider.MediaStore +import android.util.Base64 +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import com.google.android.gms.location.LocationServices +import id.ac.ubharajaya.sistemakademik.R +import id.ac.ubharajaya.sistemakademik.data.AbsensiData +import id.ac.ubharajaya.sistemakademik.data.DataHolder +import java.io.ByteArrayOutputStream + +class AbsensiFragment : Fragment() { + + private lateinit var ivPreview: ImageView + private lateinit var tvLokasi: TextView + private lateinit var btnAmbilFoto: Button + private lateinit var btnLanjut: Button + + private var foto: Bitmap? = null + private var latitude: Double? = null + private var longitude: Double? = null + + private val fusedLocationClient by lazy { + LocationServices.getFusedLocationProviderClient(requireActivity()) + } + + // Permission launcher untuk lokasi + private val locationPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { granted -> + if (granted) { + ambilLokasiGPS() + } else { + Toast.makeText(context, "Izin lokasi ditolak", Toast.LENGTH_SHORT).show() + } + } + + // Camera launcher + private val cameraLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val bitmap = result.data?.extras?.get("data") as? Bitmap + if (bitmap != null) { + foto = bitmap + ivPreview.setImageBitmap(bitmap) + cekKelengkapanData() + Toast.makeText(context, "Foto berhasil diambil!", Toast.LENGTH_SHORT).show() + } + } + } + + // Permission launcher untuk kamera + private val cameraPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { granted -> + if (granted) { + bukaKamera() + } else { + Toast.makeText(context, "Izin kamera ditolak", Toast.LENGTH_SHORT).show() + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_absensi, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + ivPreview = view.findViewById(R.id.ivPreviewFoto) + tvLokasi = view.findViewById(R.id.tvLokasi) + btnAmbilFoto = view.findViewById(R.id.btnAmbilFoto) + btnLanjut = view.findViewById(R.id.btnLanjutPreview) + + // Cek profile sudah diisi atau belum + if (DataHolder.userProfile.nama.isEmpty()) { + Toast.makeText(context, "Isi profile dulu di tab Profil!", Toast.LENGTH_LONG).show() + } + + // Request permission lokasi + requestLocationPermission() + + btnAmbilFoto.setOnClickListener { + requestCameraPermission() + } + + btnLanjut.setOnClickListener { + if (foto != null && latitude != null && longitude != null) { + simpanDataSementara() + findNavController().navigate(R.id.action_absensi_to_preview) + } + } + } + + private fun requestLocationPermission() { + when { + ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED -> { + ambilLokasiGPS() + } + else -> { + locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) + } + } + } + + private fun ambilLokasiGPS() { + try { + fusedLocationClient.lastLocation.addOnSuccessListener { location -> + if (location != null) { + latitude = location.latitude + longitude = location.longitude + tvLokasi.text = "๐Ÿ“ GPS Ready\nLat: ${location.latitude}\nLon: ${location.longitude}" + cekKelengkapanData() + } else { + tvLokasi.text = "โŒ Lokasi tidak tersedia\nHidupkan GPS" + } + } + } catch (e: SecurityException) { + Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_SHORT).show() + } + } + + private fun requestCameraPermission() { + when { + ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED -> { + bukaKamera() + } + else -> { + cameraPermissionLauncher.launch(Manifest.permission.CAMERA) + } + } + } + + private fun bukaKamera() { + val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + cameraLauncher.launch(intent) + } + + private fun cekKelengkapanData() { + btnLanjut.isEnabled = foto != null && latitude != null && longitude != null + } + + private fun simpanDataSementara() { + val bitmap = foto ?: return + val lat = latitude ?: return + val lon = longitude ?: return + + val fotoBase64 = bitmapToBase64(bitmap) + + DataHolder.currentAbsensi = AbsensiData( + nama = DataHolder.userProfile.nama, + npm = DataHolder.userProfile.npm, + latitude = lat, + longitude = lon, + timestamp = System.currentTimeMillis(), + fotoBase64 = fotoBase64, + alamat = "Alamat akan diambil saat preview" + ) + } + + private fun bitmapToBase64(bitmap: Bitmap): String { + val outputStream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream) + return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP) + } +} \ No newline at end of file diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/fragments/HistoryFragment.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/fragments/HistoryFragment.kt index e99dc5b..a5ceb42 100644 --- a/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/fragments/HistoryFragment.kt +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/fragments/HistoryFragment.kt @@ -1,2 +1,57 @@ package id.ac.ubharajaya.sistemakademik.ui.fragments +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import id.ac.ubharajaya.sistemakademik.R +import id.ac.ubharajaya.sistemakademik.adapter.HistoryAdapter +import id.ac.ubharajaya.sistemakademik.data.DataHolder + +class HistoryFragment : Fragment() { + + private lateinit var rvHistory: RecyclerView + private lateinit var tvEmpty: TextView + private lateinit var adapter: HistoryAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_history, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + rvHistory = view.findViewById(R.id.rvHistory) + tvEmpty = view.findViewById(R.id.tvEmptyState) + + adapter = HistoryAdapter(DataHolder.historyList) + rvHistory.layoutManager = LinearLayoutManager(context) + rvHistory.adapter = adapter + + cekDataKosong() + } + + override fun onResume() { + super.onResume() + adapter.notifyDataSetChanged() + cekDataKosong() + } + + private fun cekDataKosong() { + if (DataHolder.historyList.isEmpty()) { + tvEmpty.visibility = View.VISIBLE + rvHistory.visibility = View.GONE + } else { + tvEmpty.visibility = View.GONE + rvHistory.visibility = View.VISIBLE + } + } +} \ No newline at end of file diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/fragments/PreviewFragment.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/fragments/PreviewFragment.kt index e99dc5b..3a84f2d 100644 --- a/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/fragments/PreviewFragment.kt +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/fragments/PreviewFragment.kt @@ -1,2 +1,173 @@ package id.ac.ubharajaya.sistemakademik.ui.fragments +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.os.Bundle +import android.util.Base64 +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.TextView +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import id.ac.ubharajaya.sistemakademik.R +import id.ac.ubharajaya.sistemakademik.data.DataHolder +import org.json.JSONObject +import java.net.HttpURLConnection +import java.net.URL +import java.text.SimpleDateFormat +import java.util.* +import kotlin.concurrent.thread + +class PreviewFragment : Fragment() { + + private lateinit var ivFoto: ImageView + private lateinit var tvNama: TextView + private lateinit var tvNpm: TextView + private lateinit var tvTimestamp: TextView + private lateinit var tvKoordinat: TextView + private lateinit var tvAlamat: TextView + private lateinit var tvStatus: TextView + private lateinit var btnSubmit: Button + private lateinit var btnKembali: Button + private lateinit var progressBar: ProgressBar + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_preview, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + ivFoto = view.findViewById(R.id.ivFotoPreview) + tvNama = view.findViewById(R.id.tvNama) + tvNpm = view.findViewById(R.id.tvNpm) + tvTimestamp = view.findViewById(R.id.tvTimestamp) + tvKoordinat = view.findViewById(R.id.tvKoordinat) + tvAlamat = view.findViewById(R.id.tvAlamat) + tvStatus = view.findViewById(R.id.tvStatusValidasi) + btnSubmit = view.findViewById(R.id.btnSubmitAbsensi) + btnKembali = view.findViewById(R.id.btnKembali) + progressBar = view.findViewById(R.id.progressBar) + + tampilkanDataPreview() + + btnSubmit.setOnClickListener { + kirimAbsensi() + } + + btnKembali.setOnClickListener { + findNavController().popBackStack() + } + } + + private fun tampilkanDataPreview() { + val absensi = DataHolder.currentAbsensi ?: run { + Toast.makeText(context, "Data tidak ditemukan", Toast.LENGTH_SHORT).show() + findNavController().popBackStack() + return + } + + // Tampilkan foto + val bitmap = base64ToBitmap(absensi.fotoBase64) + ivFoto.setImageBitmap(bitmap) + + // Tampilkan data mahasiswa + tvNama.text = "๐Ÿ‘ค Nama: ${absensi.nama}" + tvNpm.text = "๐ŸŽ“ NPM: ${absensi.npm}" + + // Tampilkan timestamp + val dateFormat = SimpleDateFormat("dd MMM yyyy, HH:mm:ss", Locale("id", "ID")) + val tanggal = dateFormat.format(Date(absensi.timestamp)) + tvTimestamp.text = "โฐ $tanggal WIB" + + // Tampilkan koordinat + tvKoordinat.text = "๐ŸŒ Lat: ${absensi.latitude}, Lon: ${absensi.longitude}" + + // Tampilkan alamat (dummy dulu) + tvAlamat.text = "๐Ÿ“ฎ ${absensi.alamat}" + + // Status validasi (dummy - selalu valid) + tvStatus.text = "โœ… Dalam Radius Kampus (0m dari lokasi)" + tvStatus.setTextColor(resources.getColor(android.R.color.holo_green_dark, null)) + } + + private fun base64ToBitmap(base64: String): Bitmap { + val decodedBytes = Base64.decode(base64, Base64.NO_WRAP) + return BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size) + } + + private fun kirimAbsensi() { + val absensi = DataHolder.currentAbsensi ?: return + + // Tampilkan loading + progressBar.visibility = View.VISIBLE + btnSubmit.isEnabled = false + btnKembali.isEnabled = false + + thread { + try { + val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254") + val conn = url.openConnection() as HttpURLConnection + + conn.requestMethod = "POST" + conn.setRequestProperty("Content-Type", "application/json") + conn.doOutput = true + + val json = JSONObject().apply { + put("npm", absensi.npm) + put("nama", absensi.nama) + put("latitude", absensi.latitude) + put("longitude", absensi.longitude) + put("timestamp", absensi.timestamp) + put("foto_base64", absensi.fotoBase64) + put("address", absensi.alamat) + put("distance_from_campus", 0) + put("status", "hadir") + } + + conn.outputStream.use { + it.write(json.toString().toByteArray()) + } + + val responseCode = conn.responseCode + + activity?.runOnUiThread { + progressBar.visibility = View.GONE + btnSubmit.isEnabled = true + btnKembali.isEnabled = true + + if (responseCode == 200) { + Toast.makeText(context, "โœ… Absensi berhasil dikirim!", Toast.LENGTH_LONG).show() + + // Simpan ke history + DataHolder.historyList.add(0, absensi) + + // Kembali ke halaman absensi + findNavController().popBackStack() + } else { + Toast.makeText(context, "โŒ Gagal kirim (Code: $responseCode)", Toast.LENGTH_SHORT).show() + } + } + + conn.disconnect() + + } catch (e: Exception) { + activity?.runOnUiThread { + progressBar.visibility = View.GONE + btnSubmit.isEnabled = true + btnKembali.isEnabled = true + Toast.makeText(context, "โŒ Error: ${e.message}", Toast.LENGTH_SHORT).show() + } + } + } + } +} diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/fragments/ProfileFragment.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/fragments/ProfileFragment.kt index e99dc5b..e0a40de 100644 --- a/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/fragments/ProfileFragment.kt +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/fragments/ProfileFragment.kt @@ -1,2 +1,67 @@ package id.ac.ubharajaya.sistemakademik.ui.fragments +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView +import android.widget.Toast +import androidx.fragment.app.Fragment +import com.google.android.material.textfield.TextInputEditText +import id.ac.ubharajaya.sistemakademik.R +import id.ac.ubharajaya.sistemakademik.data.DataHolder +class ProfileFragment : Fragment() { + + private lateinit var etNama: TextInputEditText + private lateinit var etNpm: TextInputEditText + private lateinit var btnSimpan: Button + private lateinit var tvInfo: TextView + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_profile, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + etNama = view.findViewById(R.id.etNama) + etNpm = view.findViewById(R.id.etNpm) + btnSimpan = view.findViewById(R.id.btnSimpanProfile) + tvInfo = view.findViewById(R.id.tvProfileInfo) + + // Load data yang sudah disimpan + etNama.setText(DataHolder.userProfile.nama) + etNpm.setText(DataHolder.userProfile.npm) + + if (DataHolder.userProfile.nama.isNotEmpty()) { + tampilkanInfo() + } + + btnSimpan.setOnClickListener { + val nama = etNama.text.toString().trim() + val npm = etNpm.text.toString().trim() + + if (nama.isEmpty() || npm.isEmpty()) { + Toast.makeText(context, "Nama dan NPM harus diisi!", Toast.LENGTH_SHORT).show() + return@setOnClickListener + } + + // Simpan ke DataHolder + DataHolder.userProfile.nama = nama + DataHolder.userProfile.npm = npm + + Toast.makeText(context, "Profile berhasil disimpan!", Toast.LENGTH_SHORT).show() + tampilkanInfo() + } + } + + private fun tampilkanInfo() { + tvInfo.visibility = View.VISIBLE + tvInfo.text = "โœ… Profile tersimpan\n${DataHolder.userProfile.nama}\n${DataHolder.userProfile.npm}" + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index cdc89f2..f389cd5 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,6 +1,29 @@ - + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_absensi.xml b/app/src/main/res/layout/fragment_absensi.xml index cdc89f2..56d0dc6 100644 --- a/app/src/main/res/layout/fragment_absensi.xml +++ b/app/src/main/res/layout/fragment_absensi.xml @@ -1,6 +1,51 @@ - + - \ No newline at end of file + + + + + + +