Initial commit
This commit is contained in:
parent
ed435ffbc1
commit
462f9c315a
13
.idea/deviceManager.xml
generated
Normal file
13
.idea/deviceManager.xml
generated
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceTable">
|
||||||
|
<option name="columnSorters">
|
||||||
|
<list>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Name" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
50
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
50
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
8
.idea/markdown.xml
generated
Normal file
8
.idea/markdown.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MarkdownSettings">
|
||||||
|
<option name="previewPanelProviderInfo">
|
||||||
|
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -6,6 +6,7 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.camera"
|
android:name="android.hardware.camera"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
@ -19,17 +20,27 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.SistemAkademik">
|
android:theme="@style/Theme.SistemAkademik">
|
||||||
|
|
||||||
|
<!-- 🔥 SPLASH SCREEN (LAUNCHER) -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".SplashActivity"
|
||||||
android:exported="true"
|
android:exported="true">
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/Theme.SistemAkademik">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!-- LOGIN -->
|
||||||
|
<activity
|
||||||
|
android:name=".LoginActivity"
|
||||||
|
android:exported="true"/>
|
||||||
|
|
||||||
|
<!-- MAIN -->
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"/>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
44
app/src/main/java/id/ac/ubharajaya/sistemakademik/Absensi.kt
Normal file
44
app/src/main/java/id/ac/ubharajaya/sistemakademik/Absensi.kt
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package id.ac.ubharajaya.sistemakademik
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.util.Base64
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class untuk menyimpan informasi absensi mahasiswa
|
||||||
|
*/
|
||||||
|
data class Absensi(
|
||||||
|
val npm: String,
|
||||||
|
val nama: String,
|
||||||
|
val latitude: Double,
|
||||||
|
val longitude: Double,
|
||||||
|
val waktu: String,
|
||||||
|
val foto: Bitmap? // ⬅️ HARUS nullable
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Convert objek Absensi menjadi JSON
|
||||||
|
*/
|
||||||
|
fun toJson(): JSONObject {
|
||||||
|
val json = JSONObject()
|
||||||
|
json.put("npm", npm)
|
||||||
|
json.put("nama", nama)
|
||||||
|
json.put("latitude", latitude)
|
||||||
|
json.put("longitude", longitude)
|
||||||
|
json.put("waktu", waktu)
|
||||||
|
json.put("timestamp", System.currentTimeMillis())
|
||||||
|
json.put("foto_base64", foto?.let { bitmapToBase64(it) })
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun bitmapToBase64(bitmap: Bitmap): String {
|
||||||
|
val outputStream = ByteArrayOutputStream()
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
|
||||||
|
return Base64.encodeToString(
|
||||||
|
outputStream.toByteArray(),
|
||||||
|
Base64.NO_WRAP
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,182 @@
|
|||||||
|
package id.ac.ubharajaya.sistemakademik
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.AccountCircle
|
||||||
|
import androidx.compose.material.icons.filled.Lock
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
class LoginActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContent {
|
||||||
|
|
||||||
|
val typography = Typography(
|
||||||
|
titleLarge = MaterialTheme.typography.titleLarge.copy(
|
||||||
|
fontFamily = FontFamily.SansSerif,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 22.sp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
typography = typography,
|
||||||
|
colorScheme = if (isSystemInDarkTheme())
|
||||||
|
darkColorScheme()
|
||||||
|
else
|
||||||
|
lightColorScheme()
|
||||||
|
) {
|
||||||
|
LoginScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoginScreen() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var npm by remember { mutableStateOf("") }
|
||||||
|
var password by remember { mutableStateOf("") }
|
||||||
|
var showCard by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) { showCard = true }
|
||||||
|
|
||||||
|
val buttonGradient = Brush.horizontalGradient(
|
||||||
|
listOf(Color(0xFF1565C0), Color(0xFF42A5F5))
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black) // 🔥 BACKGROUND HITAM
|
||||||
|
.padding(24.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = showCard,
|
||||||
|
enter = fadeIn() + scaleIn(initialScale = 0.9f)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(22.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(14.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(28.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.logo),
|
||||||
|
contentDescription = "Logo",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(120.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Absensi PPB",
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Login Mahasiswa",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(28.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = npm,
|
||||||
|
onValueChange = { npm = it },
|
||||||
|
label = { Text("NPM") },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(Icons.Default.AccountCircle, contentDescription = null)
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = password,
|
||||||
|
onValueChange = { password = it },
|
||||||
|
label = { Text("Password") },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(Icons.Default.Lock, contentDescription = null)
|
||||||
|
},
|
||||||
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (npm == "202310715307" && password == "yoo23") {
|
||||||
|
context.startActivity(
|
||||||
|
Intent(context, MainActivity::class.java)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"NPM atau password salah",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
|
||||||
|
contentPadding = PaddingValues(),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(48.dp)
|
||||||
|
.background(
|
||||||
|
brush = buttonGradient,
|
||||||
|
shape = RoundedCornerShape(14.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Login",
|
||||||
|
color = Color.White,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,110 +3,203 @@ package id.ac.ubharajaya.sistemakademik
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Base64
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.google.android.gms.location.LocationServices
|
import com.google.android.gms.location.LocationServices
|
||||||
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
|
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
|
||||||
import org.json.JSONObject
|
import kotlinx.coroutines.delay
|
||||||
import java.io.ByteArrayOutputStream
|
import kotlinx.coroutines.launch
|
||||||
import java.net.HttpURLConnection
|
import java.text.SimpleDateFormat
|
||||||
import java.net.URL
|
import java.util.*
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
/* ================= UTIL ================= */
|
/* ================== KONFIG LOCKOUT ================== */
|
||||||
|
const val LOCKOUT_DURATION = 10 * 60 * 1000L // 1 menit
|
||||||
|
|
||||||
fun bitmapToBase64(bitmap: Bitmap): String {
|
/* ================== REPOSITORY ================== */
|
||||||
val outputStream = ByteArrayOutputStream()
|
class AbsensiRepository {
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
|
fun kirimAbsensi(absensi: Absensi, onResult: (Boolean) -> Unit) {
|
||||||
return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun kirimKeN8n(
|
|
||||||
context: ComponentActivity,
|
|
||||||
latitude: Double,
|
|
||||||
longitude: Double,
|
|
||||||
foto: Bitmap
|
|
||||||
) {
|
|
||||||
thread {
|
thread {
|
||||||
try {
|
try {
|
||||||
val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254")
|
val url = java.net.URL(
|
||||||
// test URL val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook-test/23c6993d-1792-48fb-ad1c-ffc78a3e6254")
|
"https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254"
|
||||||
val conn = url.openConnection() as HttpURLConnection
|
)
|
||||||
|
val conn = url.openConnection() as java.net.HttpURLConnection
|
||||||
conn.requestMethod = "POST"
|
conn.requestMethod = "POST"
|
||||||
conn.setRequestProperty("Content-Type", "application/json")
|
conn.setRequestProperty("Content-Type", "application/json")
|
||||||
conn.doOutput = true
|
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 {
|
conn.outputStream.use {
|
||||||
it.write(json.toString().toByteArray())
|
it.write(absensi.toJson().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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onResult(conn.responseCode == 200)
|
||||||
conn.disconnect()
|
conn.disconnect()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
onResult(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch (_: Exception) {
|
/* ================== VIEWMODEL ================== */
|
||||||
context.runOnUiThread {
|
class AbsensiViewModel(
|
||||||
|
private val repo: AbsensiRepository,
|
||||||
|
private val activity: ComponentActivity
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
var lokasi by mutableStateOf("Koordinat: -")
|
||||||
|
private set
|
||||||
|
|
||||||
|
var latitude by mutableStateOf<Double?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var longitude by mutableStateOf<Double?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var foto by mutableStateOf<Bitmap?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var absensiList = mutableStateListOf<Absensi>()
|
||||||
|
|
||||||
|
var waktuAbsensi by mutableStateOf<String?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var isLoading by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var sisaLockout by mutableStateOf(0L)
|
||||||
|
private set
|
||||||
|
|
||||||
|
init {
|
||||||
|
startLockoutCountdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateLokasi(lat: Double, lon: Double) {
|
||||||
|
latitude = lat
|
||||||
|
longitude = lon
|
||||||
|
lokasi = "Lat: $lat\nLon: $lon"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateFoto(bitmap: Bitmap) {
|
||||||
|
foto = bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLastAbsensiTime(): Long {
|
||||||
|
val prefs = activity.getSharedPreferences("absensi_prefs", Activity.MODE_PRIVATE)
|
||||||
|
return prefs.getLong("last_absensi_time", 0L)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun simpanWaktuAbsensi() {
|
||||||
|
val prefs = activity.getSharedPreferences("absensi_prefs", Activity.MODE_PRIVATE)
|
||||||
|
prefs.edit().putLong("last_absensi_time", System.currentTimeMillis()).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startLockoutCountdown() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
while (true) {
|
||||||
|
val diff = System.currentTimeMillis() - getLastAbsensiTime()
|
||||||
|
sisaLockout =
|
||||||
|
if (diff < LOCKOUT_DURATION) LOCKOUT_DURATION - diff else 0L
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun kirimAbsensi() {
|
||||||
|
if (sisaLockout > 0) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
activity,
|
||||||
"Gagal kirim ke server",
|
"Sudah absen, tunggu ${(sisaLockout / 1000) / 60} menit",
|
||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_LONG
|
||||||
).show()
|
).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latitude == null || longitude == null || foto == null) {
|
||||||
|
Toast.makeText(activity, "Lokasi atau foto belum lengkap", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val waktu = SimpleDateFormat(
|
||||||
|
"dd MMM yyyy HH:mm:ss",
|
||||||
|
Locale.getDefault()
|
||||||
|
).format(System.currentTimeMillis())
|
||||||
|
|
||||||
|
val absensi = Absensi(
|
||||||
|
npm = "202310715307",
|
||||||
|
nama = "Satrio Putra Wardani",
|
||||||
|
latitude = latitude!!,
|
||||||
|
longitude = longitude!!,
|
||||||
|
waktu = waktu,
|
||||||
|
foto = foto
|
||||||
|
)
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
repo.kirimAbsensi(absensi) { success ->
|
||||||
|
viewModelScope.launch {
|
||||||
|
isLoading = false
|
||||||
|
if (success) {
|
||||||
|
absensiList.add(absensi)
|
||||||
|
waktuAbsensi = waktu
|
||||||
|
simpanWaktuAbsensi()
|
||||||
|
Toast.makeText(activity, "Absensi berhasil", Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(activity, "Gagal kirim absensi", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================= ACTIVITY ================= */
|
/* ================== MAIN ACTIVITY ================== */
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
private lateinit var viewModel: AbsensiViewModel
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
viewModel = AbsensiViewModel(AbsensiRepository(), this)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
SistemAkademikTheme {
|
SistemAkademikTheme {
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
Scaffold(
|
||||||
|
topBar = { TopBarLogout(this) }
|
||||||
|
) { padding ->
|
||||||
AbsensiScreen(
|
AbsensiScreen(
|
||||||
modifier = Modifier.padding(innerPadding),
|
modifier = Modifier.padding(padding),
|
||||||
activity = this
|
viewModel = viewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,161 +207,174 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================= UI ================= */
|
/* ================== TOP BAR ================== */
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AbsensiScreen(
|
fun TopBarLogout(activity: ComponentActivity) {
|
||||||
modifier: Modifier = Modifier,
|
val context = LocalContext.current
|
||||||
activity: ComponentActivity
|
TopAppBar(
|
||||||
) {
|
title = {
|
||||||
|
Text(
|
||||||
|
" Sistem Akademik ",
|
||||||
|
style = MaterialTheme.typography.titleLarge.copy(
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
val intent = Intent(context, LoginActivity::class.java)
|
||||||
|
intent.flags =
|
||||||
|
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
context.startActivity(intent)
|
||||||
|
}) {
|
||||||
|
Text(
|
||||||
|
"Logout",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = Color.Red
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================== UI ================== */
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AbsensiScreen(modifier: Modifier, viewModel: AbsensiViewModel) {
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
var lokasi by remember { mutableStateOf("Koordinat: -") }
|
|
||||||
var latitude by remember { mutableStateOf<Double?>(null) }
|
|
||||||
var longitude by remember { mutableStateOf<Double?>(null) }
|
|
||||||
var foto by remember { mutableStateOf<Bitmap?>(null) }
|
|
||||||
|
|
||||||
val fusedLocationClient =
|
|
||||||
LocationServices.getFusedLocationProviderClient(context)
|
|
||||||
|
|
||||||
/* ===== Permission Lokasi ===== */
|
|
||||||
|
|
||||||
val locationPermissionLauncher =
|
val locationPermissionLauncher =
|
||||||
rememberLauncherForActivityResult(
|
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
ActivityResultContracts.RequestPermission()
|
|
||||||
) { granted ->
|
|
||||||
if (granted) {
|
if (granted) {
|
||||||
|
LocationServices.getFusedLocationProviderClient(context)
|
||||||
if (
|
.lastLocation.addOnSuccessListener {
|
||||||
ContextCompat.checkSelfPermission(
|
it?.let { loc ->
|
||||||
context,
|
viewModel.updateLokasi(loc.latitude, loc.longitude)
|
||||||
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 =
|
val cameraLauncher =
|
||||||
rememberLauncherForActivityResult(
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
ActivityResultContracts.StartActivityForResult()
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
) { result ->
|
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
|
||||||
val bitmap =
|
val bitmap =
|
||||||
result.data?.extras?.getParcelable("data", Bitmap::class.java)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||||
if (bitmap != null) {
|
it.data?.extras?.getParcelable("data", Bitmap::class.java)
|
||||||
foto = bitmap
|
else it.data?.extras?.getParcelable("data")
|
||||||
Toast.makeText(
|
bitmap?.let { bmp -> viewModel.updateFoto(bmp) }
|
||||||
context,
|
|
||||||
"Foto berhasil diambil",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val cameraPermissionLauncher =
|
val cameraPermissionLauncher =
|
||||||
rememberLauncherForActivityResult(
|
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||||
ActivityResultContracts.RequestPermission()
|
if (it) cameraLauncher.launch(Intent(MediaStore.ACTION_IMAGE_CAPTURE))
|
||||||
) { 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) {
|
LaunchedEffect(Unit) {
|
||||||
locationPermissionLauncher.launch(
|
locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== UI ===== */
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(24.dp),
|
.background(Color.White)
|
||||||
verticalArrangement = Arrangement.Center
|
.padding(24.dp)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Absensi Akademik",
|
text = "Absensi Akademik",
|
||||||
style = MaterialTheme.typography.titleLarge
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.headlineSmall.copy(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.Black
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
Text(text = lokasi)
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
elevation = CardDefaults.cardElevation(6.dp)
|
||||||
|
) {
|
||||||
|
Column(Modifier.padding(16.dp)) {
|
||||||
|
Text(
|
||||||
|
"Lokasi:",
|
||||||
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
viewModel.lokasi,
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) },
|
||||||
cameraPermissionLauncher.launch(
|
|
||||||
Manifest.permission.CAMERA
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text("Ambil Foto")
|
Text("Ambil Foto")
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = { viewModel.kirimAbsensi() },
|
||||||
if (latitude != null && longitude != null && foto != null) {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
kirimKeN8n(
|
enabled = !viewModel.isLoading
|
||||||
activity,
|
|
||||||
latitude!!,
|
|
||||||
longitude!!,
|
|
||||||
foto!!
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Lokasi atau foto belum lengkap",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
) {
|
||||||
Text("Kirim Absensi")
|
if (viewModel.isLoading)
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(22.dp),
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
else Text("Kirim Absensi")
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
LazyColumn {
|
||||||
|
items(viewModel.absensiList) { item ->
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 12.dp)
|
||||||
|
.animateContentSize()
|
||||||
|
) {
|
||||||
|
Column(Modifier.padding(16.dp)) {
|
||||||
|
Text(
|
||||||
|
item.nama,
|
||||||
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(item.npm, color = Color.White)
|
||||||
|
Text(item.waktu, color = Color.White)
|
||||||
|
item.foto?.let {
|
||||||
|
Image(
|
||||||
|
bitmap = it.asImageBitmap(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(150.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,62 @@
|
|||||||
|
package id.ac.ubharajaya.sistemakademik
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
class SplashActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContent {
|
||||||
|
SplashScreen {
|
||||||
|
startActivity(Intent(this, LoginActivity::class.java))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SplashScreen(onFinish: () -> Unit) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(2000) // 2 detik
|
||||||
|
onFinish()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔥 GRADIENT ABU-ABU MODERN
|
||||||
|
val gradient = Brush.verticalGradient(
|
||||||
|
listOf(
|
||||||
|
Color(0xFF2C2C2C), // Dark Grey
|
||||||
|
Color(0xFF757575) // Light Grey
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(gradient),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.logo),
|
||||||
|
contentDescription = "Logo",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(140.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
app/src/main/res/drawable/logo.png
Normal file
BIN
app/src/main/res/drawable/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
app/src/main/res/logo.png
Normal file
BIN
app/src/main/res/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
Loading…
x
Reference in New Issue
Block a user