From 46a9b5b82ebd9c1336430f63009c007488b2e353 Mon Sep 17 00:00:00 2001 From: Nucha Date: Thu, 12 Jun 2025 14:54:44 +0700 Subject: [PATCH] update --- .idea/deploymentTargetSelector.xml | 8 ++ app/build.gradle.kts | 4 + app/src/main/AndroidManifest.xml | 5 +- .../attendancemanager/ApiClient.kt | 31 +++-- .../attendancemanager/AttendanceViewModel.kt | 33 +++++ .../attendancemanager/AuthApiService.kt | 10 +- .../attendancemanager/AuthViewModel.kt | 59 ++++---- .../attendancemanager/CheckInOutScreen.kt | 128 ++++++++++++++++-- .../attendancemanager/CustomButton.kt | 14 +- .../attendancemanager/LoginScreen.kt | 7 +- .../attendancemanager/MainActivity.kt | 6 +- gradle/libs.versions.toml | 2 + 12 files changed, 246 insertions(+), 61 deletions(-) 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" }