diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index b268ef3..0337454 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,6 +4,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index bdbcbf8..a471229 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -41,6 +41,9 @@ android {
}
dependencies {
+ val composeBom = platform("androidx.compose:compose-bom:2025.05.00")
+ implementation(composeBom)
+ androidTestImplementation(composeBom)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
@@ -62,6 +65,7 @@ dependencies {
implementation(libs.androidx.navigation.fragment.ktx)
implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.ads.mobile.sdk)
+ implementation(libs.accompanist.permissions)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 192eb4d..a8adc68 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,7 +1,8 @@
-
+
+
+
+
Unit) {
+ // สร้าง request body จากพิกัดที่รับเข้ามา
+ val body = CheckInRequest(lat = lat, long = long)
+
+ // ใช้ viewModelScope เพื่อ launch coroutine ทำงานแบบ background
+ viewModelScope.launch {
+ try {
+ // เรียก API check-in ผ่าน Retrofit โดยส่ง token ใน header แบบ Bearer และ body ใน POST
+ val response = ApiClient.attendanceApi.checkIn("Bearer $token", body)
+
+ if (response.isSuccessful) {
+ // ถ้า response สำเร็จ ดึงข้อความเวลาที่เช็คอินสำเร็จจาก response body
+ val msg = response.body()?.checked_in_at ?: "Check-in สำเร็จ"
+ // ส่ง callback กลับว่า success พร้อมข้อความ
+ callback(true, msg)
+ } else {
+ // กรณี response ล้มเหลว ส่ง callback พร้อมสถานะล้มเหลวและรหัส HTTP
+ callback(false, "Check-in ล้มเหลว: ${response.code()}")
+ }
+ } catch (e: Exception) {
+ // กรณีเกิดข้อผิดพลาดระหว่างเรียก API เช่น network error
+ callback(false, "เกิดข้อผิดพลาด: ${e.localizedMessage}")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/yourcompany/attendancemanager/AuthApiService.kt b/app/src/main/java/com/yourcompany/attendancemanager/AuthApiService.kt
index 513a045..bbf9f09 100644
--- a/app/src/main/java/com/yourcompany/attendancemanager/AuthApiService.kt
+++ b/app/src/main/java/com/yourcompany/attendancemanager/AuthApiService.kt
@@ -1,8 +1,14 @@
import retrofit2.http.Body
import retrofit2.http.POST
import retrofit2.Response
-
interface AuthApiService {
+
+ // ฟังก์ชันสำหรับเรียก API login ด้วย HTTP POST ไปที่ "/auth/login"
+ // ใช้ coroutine (suspend) เพื่อให้สามารถเรียกแบบ asynchronous ได้
+ // รับพารามิเตอร์เป็น LoginRequest (username/password)
+ // และคืนค่ากลับเป็น Response ที่มี token หรือข้อมูลอื่นใน response
@POST("/auth/login")
suspend fun login(@Body request: LoginRequest): Response
-}
\ No newline at end of file
+}
+
+//suspend คือ คีย์เวิร์ดในภาษา Kotlin ที่ใช้สำหรับระบุว่า ฟังก์ชันสามารถหยุดทำงานชั่วคราว (suspend) และ resume ต่อได้ภายหลัง ซึ่งเป็นพื้นฐานของ Coroutine เพื่อให้สามารถทำงานแบบ asynchronous (ไม่บล็อก UI) ได้ง่ายและปลอดภัยกว่าแบบเดิม (เช่น AsyncTask หรือ Thread)
\ No newline at end of file
diff --git a/app/src/main/java/com/yourcompany/attendancemanager/AuthViewModel.kt b/app/src/main/java/com/yourcompany/attendancemanager/AuthViewModel.kt
index 34f6941..7aad3de 100644
--- a/app/src/main/java/com/yourcompany/attendancemanager/AuthViewModel.kt
+++ b/app/src/main/java/com/yourcompany/attendancemanager/AuthViewModel.kt
@@ -1,5 +1,6 @@
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
data class LoginRequest(
val email: String,
@@ -17,35 +18,35 @@ class AuthViewModel : ViewModel() {
callback(false, "Email หรือ Password ว่าง")
return@launch
}
-//
-// // จำลองการเรียก API ที่ใช้เวลาประมาณ 2 วินาที
-// delay(2000)
-//
-//
-//
-// // ตัวอย่าง login จำลอง: email = admin@example.com, password = 1234
-// if (email == "admin@example.com" && password == "1234") {
-// callback(true, null)
-// } else {
-// callback(false, "อีเมลหรือรหัสผ่านไม่ถูกต้อง")
-// }
-// }
- try {
- val response = ApiClient.authApi.login(LoginRequest(email, password))
- if (response.isSuccessful) {
- val token = response.body()?.token
- if (!token.isNullOrBlank()) {
- // ✅ login สำเร็จ
- callback(true, null)
- } else {
- callback(false, "ไม่พบ token")
- }
- } else {
- callback(false, "อีเมลหรือรหัสผ่านไม่ถูกต้อง")
- }
- } catch (e: Exception) {
- callback(false, "เกิดข้อผิดพลาดในการเชื่อมต่อ: ${e.localizedMessage}")
+
+ // จำลองการเรียก API ที่ใช้เวลาประมาณ 2 วินาที
+ delay(2000)
+
+
+
+ // ตัวอย่าง login จำลอง: email = admin@example.com, password = 1234
+ if (email == "test@test.com" && password == "1234") {
+ callback(true, null)
+ } else {
+ callback(false, "อีเมลหรือรหัสผ่านไม่ถูกต้อง")
}
- }
+ }
+// try {
+// val response = ApiClient.authApi.login(LoginRequest(email, password))
+// if (response.isSuccessful) {
+// val token = response.body()?.token
+// if (!token.isNullOrBlank()) {
+// // ✅ login สำเร็จ
+// callback(true, null)
+// } else {
+// callback(false, "ไม่พบ token")
+// }
+// } else {
+// callback(false, "อีเมลหรือรหัสผ่านไม่ถูกต้อง")
+// }
+// } catch (e: Exception) {
+// callback(false, "เกิดข้อผิดพลาดในการเชื่อมต่อ: ${e.localizedMessage}")
+// }
+// }
}
}
diff --git a/app/src/main/java/com/yourcompany/attendancemanager/CheckInOutScreen.kt b/app/src/main/java/com/yourcompany/attendancemanager/CheckInOutScreen.kt
index 0f4a2a1..55648be 100644
--- a/app/src/main/java/com/yourcompany/attendancemanager/CheckInOutScreen.kt
+++ b/app/src/main/java/com/yourcompany/attendancemanager/CheckInOutScreen.kt
@@ -1,14 +1,126 @@
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
+// นำเข้า permission, location และ library ที่เกี่ยวข้อง
+import android.Manifest
+import android.content.pm.PackageManager
+import android.widget.Toast
+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.app.ActivityCompat
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.google.android.gms.location.LocationServices
+import com.google.accompanist.permissions.*
+/**
+ * ข้อมูลที่ได้จากการตอบกลับของ API หลังจาก Check In สำเร็จ
+ */
+data class CheckInResponse(
+ val checked_in_at: String
+)
+
+/**
+ * ข้อมูลที่ต้องส่งให้ API เพื่อทำการ Check In
+ */
+data class CheckInRequest(
+ val lat: Double,
+ val long: Double
+)
+
+/**
+ * Composable ที่ใช้แสดงหน้าจอ Check In/Out และจัดการการร้องขอ permission
+ */
+@OptIn(ExperimentalPermissionsApi::class) // ใช้ API ที่ยังเป็น experimental จาก Accompanist
@Composable
fun CheckInOutScreen() {
- Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
- Text("Check In / Out Screen")
+ // ใช้ Accompanist ในการจัดการ permission สำหรับ location
+ val locationPermissionState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
+
+ // Context ของแอป สำหรับแสดง Toast หรือใช้ API อื่น ๆ//
+ val ctx = LocalContext.current
+
+ // ใช้ remember เก็บข้อความที่ใช้แสดงผลลัพธ์จาก server
+ val message = remember { mutableStateOf(null) }
+
+ // ดึง AttendanceViewModel เพื่อเรียกใช้ฟังก์ชัน checkIn
+ val viewModel: AttendanceViewModel = viewModel()
+
+ // token จำลองไว้เรียก API
+ val token = "fake-jwt-token"
+
+ // สร้าง fusedLocationClient สำหรับเข้าถึง location ล่าสุดของผู้ใช้
+ val fusedLocationClient = remember {
+ LocationServices.getFusedLocationProviderClient(ctx)
}
-}
\ No newline at end of file
+
+ /**
+ * ฟังก์ชันเรียก permission และเรียกใช้ fusedLocationClient เพื่อดึงตำแหน่ง
+ * แล้วส่งไปยัง API สำหรับ Check In
+ */
+ fun getLocationAndCheckIn() {
+ // ถ้ายังไม่ได้รับ permission ให้เรียกขอ permission แล้วออกจากฟังก์ชันก่อน
+ if (locationPermissionState.status != PermissionStatus.Granted) {
+ locationPermissionState.launchPermissionRequest()
+ return
+ }
+
+ try {
+ // เมื่อได้รับ permission แล้ว ใช้ fusedLocationClient ดึงตำแหน่งล่าสุด
+ fusedLocationClient.lastLocation.addOnSuccessListener { location ->
+ if (location != null) {
+ val lat = location.latitude
+ val long = location.longitude
+
+ // เรียก ViewModel เพื่อติดต่อ API และส่งพิกัดไป
+ viewModel.checkIn(token, lat, long) { success, msg ->
+ message.value = msg
+ Toast.makeText(ctx, msg, Toast.LENGTH_SHORT).show()
+ }
+ } else {
+ Toast.makeText(ctx, "ไม่สามารถดึงพิกัดได้", Toast.LENGTH_SHORT).show()
+ }
+ }
+ } catch (e: SecurityException) {
+ // ถ้าเกิด SecurityException แสดงว่า permission ไม่ได้เปิดไว้จริง
+ Toast.makeText(ctx, "ไม่มีสิทธิ์เข้าถึงตำแหน่ง", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ // UI Layout แบบ Column
+ Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
+ // แสดงหัวข้อของหน้าจอ
+ Text("Check In / Out Screen", style = MaterialTheme.typography.headlineMedium)
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // ตรวจสอบสถานะ permission และแสดง UI ที่เหมาะสม
+ when (val status = locationPermissionState.status) {
+ is PermissionStatus.Granted -> {
+ // ถ้าอนุญาตแล้ว แสดงปุ่ม Check In
+ Button(onClick = { getLocationAndCheckIn() }) {
+ Text("Check In")
+ }
+ }
+ is PermissionStatus.Denied -> {
+ if (status.shouldShowRationale) {
+ // ผู้ใช้ปฏิเสธแบบชั่วคราว ให้แสดงเหตุผลและปุ่มขอใหม่
+ Text("แอปต้องขออนุญาตเข้าถึงตำแหน่ง เพื่อทำการ Check In")
+ Spacer(modifier = Modifier.height(8.dp))
+ Button(onClick = { locationPermissionState.launchPermissionRequest() }) {
+ Text("ขออนุญาต")
+ }
+ } else {
+ // ผู้ใช้ปฏิเสธถาวร (Don't ask again) แจ้งให้ไปเปิดใน Settings
+ Text("กรุณาเปิดสิทธิ์ตำแหน่งในตั้งค่าเครื่อง")
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // แสดงข้อความตอบกลับจาก server ถ้ามี
+ message.value?.let {
+ Text("Server Response: $it")
+ }
+ }
+}
diff --git a/app/src/main/java/com/yourcompany/attendancemanager/CustomButton.kt b/app/src/main/java/com/yourcompany/attendancemanager/CustomButton.kt
index 56a9616..bff3e79 100644
--- a/app/src/main/java/com/yourcompany/attendancemanager/CustomButton.kt
+++ b/app/src/main/java/com/yourcompany/attendancemanager/CustomButton.kt
@@ -4,12 +4,20 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+/**
+ * ปุ่มแบบกำหนดเอง (CustomButton) สำหรับใช้ซ้ำในหลาย ๆ ส่วนของ UI
+ *
+ * @param text ข้อความที่จะแสดงบนปุ่ม
+ * @param onClick ฟังก์ชันที่จะถูกเรียกเมื่อผู้ใช้กดปุ่ม
+ */
@Composable
fun CustomButton(text: String, onClick: () -> Unit) {
+ // ปุ่มจาก Material3 ที่ใช้คลิกได้
Button(
- onClick = onClick,
- modifier = Modifier.fillMaxWidth()
+ onClick = onClick, // ฟังก์ชันที่จะถูกเรียกเมื่อกดปุ่ม
+ modifier = Modifier.fillMaxWidth() // ปุ่มขยายเต็มความกว้างของหน้าจอ
) {
+ // ข้อความที่จะแสดงอยู่ในปุ่ม
Text(text)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/yourcompany/attendancemanager/LoginScreen.kt b/app/src/main/java/com/yourcompany/attendancemanager/LoginScreen.kt
index 7318a68..0256183 100644
--- a/app/src/main/java/com/yourcompany/attendancemanager/LoginScreen.kt
+++ b/app/src/main/java/com/yourcompany/attendancemanager/LoginScreen.kt
@@ -17,6 +17,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.compose.rememberNavController
import com.yourcompany.attendancemanager.R
@Composable
@@ -29,7 +30,10 @@ fun LoginScreen(onLoginSuccess: () -> Unit) {
val loginFailedText = stringResource(R.string.login_failed)
val isLoading = remember { mutableStateOf(false) }
- Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
+
+ Column(modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp)) {
Text(text = loginText, style = MaterialTheme.typography.headlineMedium)
InputField(label = stringResource(R.string.email), text = email.value) { email.value = it }
Spacer(modifier = Modifier.height(8.dp))
@@ -50,5 +54,6 @@ fun LoginScreen(onLoginSuccess: () -> Unit) {
}
}
}
+
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/yourcompany/attendancemanager/MainActivity.kt b/app/src/main/java/com/yourcompany/attendancemanager/MainActivity.kt
index 768e6f8..9048a98 100644
--- a/app/src/main/java/com/yourcompany/attendancemanager/MainActivity.kt
+++ b/app/src/main/java/com/yourcompany/attendancemanager/MainActivity.kt
@@ -25,9 +25,9 @@ class MainActivity : ComponentActivity() {
LoginScreen(onLoginSuccess = { navController.navigate("check") })
}
composable("check") { CheckInOutScreen() }
- composable("leave") { LeaveRequestScreen() }
- composable("leaveSummary"){ LeaveSummaryScreen() }
- composable("attendanceSummary"){ AttendanceSummaryScreen() }
+// composable("leave") { LeaveRequestScreen() }
+// composable("leaveSummary"){ LeaveSummaryScreen() }
+// composable("attendanceSummary"){ AttendanceSummaryScreen() }
}
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 26385da..dee828d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -20,6 +20,7 @@ constraintlayout = "2.2.1"
navigationFragmentKtx = "2.9.0"
navigationUiKtx = "2.9.0"
adsMobileSdk = "0.16.0-alpha01"
+accompanist = "0.31.5-beta"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -48,6 +49,7 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const
androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" }
ads-mobile-sdk = { group = "com.google.android.libraries.ads.mobile.sdk", name = "ads-mobile-sdk", version.ref = "adsMobileSdk" }
+accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }