669 lines
27 KiB
Kotlin
669 lines
27 KiB
Kotlin
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.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.foundation.rememberScrollState
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
import androidx.compose.foundation.text.KeyboardOptions
|
|
import androidx.compose.foundation.verticalScroll
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.filled.DateRange
|
|
import androidx.compose.material.icons.filled.Home
|
|
import androidx.compose.material3.*
|
|
import androidx.compose.runtime.*
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.platform.LocalContext
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.text.input.KeyboardType
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.core.content.ContextCompat
|
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
import androidx.navigation.NavController
|
|
import androidx.navigation.compose.NavHost
|
|
import androidx.navigation.compose.composable
|
|
import androidx.navigation.compose.rememberNavController
|
|
import com.google.android.gms.location.LocationServices
|
|
import id.ac.ubharajaya.sistemakademik.config.AttendanceConfig
|
|
import id.ac.ubharajaya.sistemakademik.models.AttendanceState
|
|
import id.ac.ubharajaya.sistemakademik.models.AttendanceStatus
|
|
import id.ac.ubharajaya.sistemakademik.models.Course
|
|
import id.ac.ubharajaya.sistemakademik.models.LocationData
|
|
import id.ac.ubharajaya.sistemakademik.network.N8nService
|
|
import id.ac.ubharajaya.sistemakademik.ui.components.ErrorAlertCard
|
|
import id.ac.ubharajaya.sistemakademik.ui.components.LocationStatusCard
|
|
import id.ac.ubharajaya.sistemakademik.ui.components.PhotoPreviewCard
|
|
import id.ac.ubharajaya.sistemakademik.ui.components.SubmitButtonWithLoader
|
|
import id.ac.ubharajaya.sistemakademik.ui.screens.HistoryScreen
|
|
import id.ac.ubharajaya.sistemakademik.ui.screens.LoginScreen
|
|
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
|
|
import id.ac.ubharajaya.sistemakademik.ui.viewmodel.UserViewModel
|
|
import id.ac.ubharajaya.sistemakademik.utils.AuthService
|
|
import id.ac.ubharajaya.sistemakademik.utils.CourseService
|
|
import id.ac.ubharajaya.sistemakademik.utils.ImageUtils
|
|
import id.ac.ubharajaya.sistemakademik.utils.LocationValidator
|
|
import kotlin.concurrent.thread
|
|
import androidx.compose.foundation.layout.heightIn
|
|
|
|
/* ================= ACTIVITY ================= */
|
|
|
|
class MainActivity : ComponentActivity() {
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
enableEdgeToEdge()
|
|
|
|
setContent {
|
|
SistemAkademikTheme {
|
|
AppNavigation(activity = this)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun AppNavigation(activity: ComponentActivity) {
|
|
val navController = rememberNavController()
|
|
val authService = remember { AuthService(activity) }
|
|
val userViewModel: UserViewModel = viewModel()
|
|
|
|
val startDestination = if (authService.isLoggedIn()) "main" else "login"
|
|
|
|
NavHost(navController = navController, startDestination = startDestination) {
|
|
composable("login") {
|
|
LoginScreen {
|
|
nama, npm ->
|
|
authService.login(nama, npm)
|
|
userViewModel.setUser(nama, npm)
|
|
navController.navigate("main") {
|
|
popUpTo("login") { inclusive = true }
|
|
}
|
|
}
|
|
}
|
|
composable("main") {
|
|
MainScreen(activity = activity, userViewModel = userViewModel, navController = navController)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@Composable
|
|
fun MainScreen(activity: ComponentActivity, userViewModel: UserViewModel, navController: NavController) {
|
|
val mainNavController = rememberNavController()
|
|
Scaffold(
|
|
bottomBar = {
|
|
BottomNavigationBar(navController = mainNavController)
|
|
}
|
|
) {
|
|
NavHost(
|
|
navController = mainNavController,
|
|
startDestination = "absensi",
|
|
modifier = Modifier.padding(it)
|
|
) {
|
|
composable("absensi") {
|
|
AbsensiScreen(activity = activity, userViewModel = userViewModel, appNavController = navController)
|
|
}
|
|
composable("riwayat") {
|
|
HistoryScreen()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun BottomNavigationBar(navController: androidx.navigation.NavController) {
|
|
val items = listOf(
|
|
"absensi" to Icons.Default.Home,
|
|
"riwayat" to Icons.Default.DateRange
|
|
)
|
|
var selectedItem by remember { mutableStateOf("absensi") }
|
|
|
|
NavigationBar {
|
|
items.forEach { (route, icon) ->
|
|
NavigationBarItem(
|
|
icon = { Icon(icon, contentDescription = route) },
|
|
label = { Text(route) },
|
|
selected = selectedItem == route,
|
|
onClick = {
|
|
selectedItem = route
|
|
navController.navigate(route) {
|
|
popUpTo(navController.graph.startDestinationId) {
|
|
saveState = true
|
|
}
|
|
launchSingleTop = true
|
|
restoreState = true
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ================= UI ================= */
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun AbsensiScreen(
|
|
modifier: Modifier = Modifier,
|
|
activity: ComponentActivity,
|
|
userViewModel: UserViewModel,
|
|
appNavController: NavController
|
|
) {
|
|
val context = LocalContext.current
|
|
val authService = remember { AuthService(context) }
|
|
|
|
// State management
|
|
var state by remember {
|
|
mutableStateOf(
|
|
AttendanceState()
|
|
)
|
|
}
|
|
|
|
val studentName by userViewModel.nama
|
|
val studentNpm by userViewModel.npm
|
|
var isEditing by remember { mutableStateOf(false) }
|
|
var selectedStatus by remember { mutableStateOf(AttendanceStatus.PRESENT) }
|
|
|
|
var courses by remember { mutableStateOf<List<Course>>(emptyList()) }
|
|
var selectedCourse by remember { mutableStateOf<Course?>(null) }
|
|
var showCourseSelector by remember { mutableStateOf(false) }
|
|
var searchQuery by remember { mutableStateOf("") }
|
|
|
|
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
|
|
val n8nService = remember { N8nService(activity) }
|
|
val courseService = remember { CourseService(context) }
|
|
|
|
// Initialize courses
|
|
LaunchedEffect(Unit) {
|
|
courseService.initializeSampleData()
|
|
courses = courseService.getCourses()
|
|
selectedCourse = courseService.getSelectedCourse() ?: courses.firstOrNull()
|
|
}
|
|
|
|
// Update location when location data changes
|
|
LaunchedEffect(state.location, selectedStatus) {
|
|
if (selectedStatus == AttendanceStatus.PRESENT) {
|
|
state = if (state.location != null) {
|
|
state.copy(
|
|
validationResult = state.validationResult.copy(
|
|
isValid = true, // Always valid for "Hadir" to allow submission
|
|
message = "Lokasi valid untuk status Hadir"
|
|
)
|
|
)
|
|
} else {
|
|
state.copy(
|
|
validationResult = state.validationResult.copy(
|
|
isValid = false,
|
|
message = "Mencari lokasi..."
|
|
)
|
|
)
|
|
}
|
|
} else {
|
|
state = state.copy(
|
|
validationResult = state.validationResult.copy(
|
|
isValid = true,
|
|
message = "Tidak perlu validasi lokasi untuk status ini"
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
/* ===== Permission Launchers ===== */
|
|
|
|
val locationPermissionLauncher =
|
|
rememberLauncherForActivityResult(
|
|
ActivityResultContracts.RequestPermission()
|
|
) { granted ->
|
|
if (granted) {
|
|
state = state.copy(isLoadingLocation = true)
|
|
|
|
if (
|
|
ContextCompat.checkSelfPermission(
|
|
context,
|
|
Manifest.permission.ACCESS_FINE_LOCATION
|
|
) == PackageManager.PERMISSION_GRANTED
|
|
) {
|
|
|
|
fusedLocationClient.lastLocation
|
|
.addOnSuccessListener { location ->
|
|
if (location != null) {
|
|
state = state.copy(
|
|
location = LocationData(
|
|
latitude = location.latitude,
|
|
longitude = location.longitude,
|
|
accuracy = location.accuracy
|
|
),
|
|
isLoadingLocation = false,
|
|
isLocationPermissionGranted = true
|
|
)
|
|
} else {
|
|
state = state.copy(
|
|
errorMessage = "Lokasi tidak tersedia. Pastikan GPS aktif.",
|
|
isLoadingLocation = false
|
|
)
|
|
}
|
|
}
|
|
.addOnFailureListener {
|
|
state = state.copy(
|
|
errorMessage = "Gagal mengambil lokasi: ${it.message}",
|
|
isLoadingLocation = false
|
|
)
|
|
}
|
|
}
|
|
|
|
} else {
|
|
state = state.copy(
|
|
errorMessage = "Izin lokasi ditolak. Aktifkan di pengaturan aplikasi."
|
|
)
|
|
Toast.makeText(
|
|
context,
|
|
"Izin lokasi ditolak",
|
|
Toast.LENGTH_SHORT
|
|
).show()
|
|
}
|
|
}
|
|
|
|
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) {
|
|
state = state.copy(foto = bitmap)
|
|
Toast.makeText(
|
|
context,
|
|
"✓ Foto berhasil diambil",
|
|
Toast.LENGTH_SHORT
|
|
).show()
|
|
}
|
|
}
|
|
}
|
|
|
|
val cameraPermissionLauncher =
|
|
rememberLauncherForActivityResult(
|
|
ActivityResultContracts.RequestPermission()
|
|
) { granted ->
|
|
if (granted) {
|
|
state = state.copy(isCameraPermissionGranted = true)
|
|
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
|
cameraLauncher.launch(intent)
|
|
} else {
|
|
state = state.copy(
|
|
errorMessage = "Izin kamera ditolak. Aktifkan di pengaturan aplikasi."
|
|
)
|
|
Toast.makeText(
|
|
context,
|
|
"Izin kamera ditolak",
|
|
Toast.LENGTH_SHORT
|
|
).show()
|
|
}
|
|
}
|
|
|
|
/* ===== Request Awal ===== */
|
|
|
|
LaunchedEffect(Unit) {
|
|
locationPermissionLauncher.launch(
|
|
Manifest.permission.ACCESS_FINE_LOCATION
|
|
)
|
|
}
|
|
|
|
fun getStatusLabel(status: AttendanceStatus): String {
|
|
return when (status) {
|
|
AttendanceStatus.PRESENT -> "Hadir"
|
|
AttendanceStatus.SICK -> "Sakit"
|
|
AttendanceStatus.EXCUSED -> "Izin"
|
|
else -> status.name
|
|
}
|
|
}
|
|
|
|
/* ===== UI ===== */
|
|
|
|
Column(
|
|
modifier = modifier
|
|
.fillMaxSize()
|
|
.padding(16.dp)
|
|
.verticalScroll(rememberScrollState()),
|
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
) {
|
|
|
|
Text(
|
|
text = "Absensi Akademik",
|
|
style = MaterialTheme.typography.headlineSmall
|
|
)
|
|
|
|
// Error Alert
|
|
ErrorAlertCard(
|
|
message = state.errorMessage,
|
|
onDismiss = {
|
|
state = state.copy(errorMessage = null)
|
|
}
|
|
)
|
|
|
|
// Student and Course Information Card
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
shape = RoundedCornerShape(12.dp),
|
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer),
|
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
|
) {
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(16.dp),
|
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
if (isEditing) {
|
|
OutlinedTextField(
|
|
value = studentName ?: "",
|
|
onValueChange = { userViewModel.nama.value = it },
|
|
label = { Text("Nama Mahasiswa") },
|
|
modifier = Modifier.fillMaxWidth()
|
|
)
|
|
OutlinedTextField(
|
|
value = studentNpm ?: "",
|
|
onValueChange = { userViewModel.npm.value = it },
|
|
label = { Text("NPM") },
|
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
|
modifier = Modifier.fillMaxWidth()
|
|
)
|
|
Button(onClick = { isEditing = false }) {
|
|
Text("Simpan")
|
|
}
|
|
} else {
|
|
Text(
|
|
text = "Informasi Mahasiswa",
|
|
style = MaterialTheme.typography.titleMedium,
|
|
fontWeight = FontWeight.Bold,
|
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
|
)
|
|
Text(
|
|
text = "Nama: ${studentName ?: ""}",
|
|
style = MaterialTheme.typography.bodyLarge,
|
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
|
)
|
|
Text(
|
|
text = "NPM: ${studentNpm ?: ""}",
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
|
)
|
|
Row {
|
|
Button(onClick = { isEditing = true }) {
|
|
Text("Ubah Data")
|
|
}
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
Button(onClick = {
|
|
authService.logout()
|
|
appNavController.navigate("login") {
|
|
popUpTo("main") { inclusive = true }
|
|
}
|
|
}) {
|
|
Text("Logout")
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
Divider(modifier = Modifier.padding(vertical = 4.dp))
|
|
|
|
Text(
|
|
text = "Mata Kuliah",
|
|
style = MaterialTheme.typography.titleMedium,
|
|
fontWeight = FontWeight.Bold,
|
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
|
)
|
|
Button(
|
|
onClick = { showCourseSelector = true },
|
|
modifier = Modifier.fillMaxWidth(),
|
|
colors = ButtonDefaults.buttonColors(
|
|
containerColor = MaterialTheme.colorScheme.primary
|
|
)
|
|
) {
|
|
Text(
|
|
text = selectedCourse?.courseName ?: "Pilih Mata Kuliah",
|
|
modifier = Modifier.weight(1f)
|
|
)
|
|
}
|
|
if (selectedCourse != null) {
|
|
Text(
|
|
text = "Kode: ${selectedCourse!!.courseCode} | Dosen: ${selectedCourse!!.lecturer}",
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Course Selector Dialog
|
|
if (showCourseSelector) {
|
|
AlertDialog(
|
|
onDismissRequest = { showCourseSelector = false },
|
|
title = { Text("Pilih Mata Kuliah") },
|
|
text = {
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.heightIn(max = 300.dp),
|
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
OutlinedTextField(
|
|
value = searchQuery,
|
|
onValueChange = { searchQuery = it },
|
|
label = { Text("Cari Mata Kuliah") },
|
|
modifier = Modifier.fillMaxWidth()
|
|
)
|
|
val filteredCourses = courses.filter {
|
|
it.courseName.contains(searchQuery, ignoreCase = true) ||
|
|
it.courseCode.contains(searchQuery, ignoreCase = true)
|
|
}
|
|
Column(
|
|
modifier = Modifier.verticalScroll(rememberScrollState())
|
|
) {
|
|
filteredCourses.forEach { course ->
|
|
Button(
|
|
onClick = {
|
|
selectedCourse = course
|
|
courseService.setSelectedCourse(course)
|
|
showCourseSelector = false
|
|
searchQuery = ""
|
|
},
|
|
modifier = Modifier.fillMaxWidth(),
|
|
colors = ButtonDefaults.buttonColors(
|
|
containerColor = if (selectedCourse?.courseId == course.courseId)
|
|
MaterialTheme.colorScheme.primary
|
|
else
|
|
MaterialTheme.colorScheme.secondary
|
|
)
|
|
) {
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(8.dp),
|
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
|
) {
|
|
Text(
|
|
text = course.courseName,
|
|
style = MaterialTheme.typography.labelMedium
|
|
)
|
|
Text(
|
|
text = "${course.courseCode} - ${course.lecturer}",
|
|
style = MaterialTheme.typography.labelSmall
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
confirmButton = {
|
|
TextButton(onClick = { showCourseSelector = false }) {
|
|
Text("Tutup")
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
|
|
// Location and Photo section
|
|
if (selectedStatus == AttendanceStatus.PRESENT) {
|
|
// Location Status Card
|
|
LocationStatusCard(
|
|
latitude = state.location?.latitude,
|
|
longitude = state.location?.longitude,
|
|
validationMessage = state.validationResult.message,
|
|
isLoading = state.isLoadingLocation
|
|
)
|
|
}
|
|
|
|
PhotoPreviewCard(
|
|
bitmap = state.foto,
|
|
onRetake = {
|
|
state = state.copy(foto = null)
|
|
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
|
}
|
|
)
|
|
|
|
// Attendance Status Dropdown
|
|
var expanded by remember { mutableStateOf(false) }
|
|
val items = listOf(AttendanceStatus.PRESENT, AttendanceStatus.SICK, AttendanceStatus.EXCUSED)
|
|
Box {
|
|
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = !expanded }) {
|
|
OutlinedTextField(
|
|
value = getStatusLabel(selectedStatus),
|
|
onValueChange = {},
|
|
readOnly = true,
|
|
label = { Text("Status Kehadiran") },
|
|
trailingIcon = {
|
|
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
|
|
},
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.menuAnchor()
|
|
)
|
|
ExposedDropdownMenu(
|
|
expanded = expanded,
|
|
onDismissRequest = { expanded = false }
|
|
) {
|
|
items.forEach { status ->
|
|
DropdownMenuItem(
|
|
text = { Text(getStatusLabel(status)) },
|
|
onClick = {
|
|
selectedStatus = status
|
|
expanded = false
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Buttons
|
|
Button(
|
|
onClick = {
|
|
cameraPermissionLauncher.launch(
|
|
Manifest.permission.CAMERA
|
|
)
|
|
},
|
|
modifier = Modifier.fillMaxWidth(),
|
|
enabled = !state.isLoadingSubmit
|
|
) {
|
|
Text(if (state.foto == null) "Ambil Foto" else "Ganti Foto")
|
|
}
|
|
|
|
SubmitButtonWithLoader(
|
|
text = "Kirim Absensi",
|
|
onClick = {
|
|
val isPresent = selectedStatus == AttendanceStatus.PRESENT
|
|
val canSubmit = if (isPresent) {
|
|
selectedCourse != null && state.location != null && state.foto != null && state.validationResult.isValid
|
|
} else {
|
|
selectedCourse != null && state.foto != null
|
|
}
|
|
|
|
if (canSubmit) {
|
|
state = state.copy(isLoadingSubmit = true)
|
|
|
|
val dummyLocation by lazy { LocationData(0.0, 0.0, 0.0f) }
|
|
|
|
n8nService.submitAttendanceWithCourse(
|
|
npm = studentNpm ?: "",
|
|
nama = studentName ?: "",
|
|
courseId = selectedCourse!!.courseId,
|
|
courseCode = selectedCourse!!.courseCode,
|
|
courseName = selectedCourse!!.courseName,
|
|
latitude = if (isPresent) state.location!!.latitude else dummyLocation.latitude,
|
|
longitude = if (isPresent) state.location!!.longitude else dummyLocation.longitude,
|
|
foto = state.foto!!,
|
|
isTest = false,
|
|
status = selectedStatus, // Add status to the call
|
|
callback = object : N8nService.SubmitCallback {
|
|
override fun onSuccess(responseCode: Int, message: String) {
|
|
state = state.copy(
|
|
isLoadingSubmit = false,
|
|
errorMessage = null
|
|
)
|
|
|
|
// Save to local database
|
|
val photoBase64 = state.foto?.let { ImageUtils.bitmapToBase64(it) } ?: ""
|
|
val attendance = id.ac.ubharajaya.sistemakademik.models.Attendance(
|
|
npm = studentNpm ?: "",
|
|
nama = studentName ?: "",
|
|
courseId = selectedCourse!!.courseId,
|
|
courseCode = selectedCourse!!.courseCode,
|
|
courseName = selectedCourse!!.courseName,
|
|
latitude = if (isPresent) state.location!!.latitude else dummyLocation.latitude,
|
|
longitude = if (isPresent) state.location!!.longitude else dummyLocation.longitude,
|
|
timestamp = System.currentTimeMillis(),
|
|
date = courseService.getCurrentDate(),
|
|
time = courseService.formatTime(System.currentTimeMillis()),
|
|
status = selectedStatus,
|
|
isValid = if (isPresent) state.validationResult.isValid else true,
|
|
submissionResult = "Success: $message",
|
|
photoBase64 = photoBase64
|
|
)
|
|
courseService.saveAttendance(attendance)
|
|
|
|
// Reset after successful submission
|
|
thread {
|
|
Thread.sleep(2000)
|
|
state = state.copy(
|
|
foto = null,
|
|
location = null
|
|
)
|
|
}
|
|
}
|
|
|
|
override fun onError(error: Throwable, message: String) {
|
|
state = state.copy(
|
|
isLoadingSubmit = false,
|
|
errorMessage = message
|
|
)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
},
|
|
isLoading = state.isLoadingSubmit,
|
|
isEnabled = if (selectedStatus == AttendanceStatus.PRESENT) {
|
|
selectedCourse != null && state.location != null && state.foto != null && state.validationResult.isValid
|
|
} else {
|
|
selectedCourse != null && state.foto != null
|
|
}
|
|
)
|
|
|
|
Spacer(modifier = Modifier.height(16.dp))
|
|
}
|
|
}
|