update
This commit is contained in:
parent
79367aef7f
commit
46a9b5b82e
8
.idea/deploymentTargetSelector.xml
generated
8
.idea/deploymentTargetSelector.xml
generated
@ -4,6 +4,14 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-06-12T01:51:13.726519Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/nucha/.android/avd/Medium_Phone_API_33.avd" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
@ -13,6 +14,8 @@
|
||||
android:theme="@style/Theme.AttendanceManager"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
@ -2,22 +2,25 @@ import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
|
||||
object ApiClient {
|
||||
private const val BASE_URL = "http://10.0.2.2:8000" // ใช้ 10.0.2.2 แทน localhost สำหรับ emulator
|
||||
// กำหนด BASE_URL ของ API ใช้ 10.0.2.2 แทน localhost สำหรับ Android emulator
|
||||
private const val BASE_URL = "http://10.0.2.2:8000"
|
||||
|
||||
// สร้าง Retrofit instance เดียว ใช้ lazy initialization คือสร้างเมื่อเรียกใช้ครั้งแรก
|
||||
private val retrofit: Retrofit by lazy {
|
||||
Retrofit.Builder()
|
||||
.baseUrl(BASE_URL) // กำหนด URL หลักของ API
|
||||
.addConverterFactory(GsonConverterFactory.create()) // ใช้ Gson แปลง JSON เป็น object และกลับกัน
|
||||
.build()
|
||||
}
|
||||
|
||||
// สร้าง AuthApiService จาก Retrofit instance เดียวกัน
|
||||
val authApi: AuthApiService by lazy {
|
||||
Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(AuthApiService::class.java)
|
||||
|
||||
retrofit.create(AuthApiService::class.java)
|
||||
}
|
||||
|
||||
|
||||
// สร้าง AttendanceApiService จาก Retrofit instance เดียวกัน
|
||||
val attendanceApi: AttendanceApiService by lazy {
|
||||
Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(AttendanceApiService::class.java)
|
||||
retrofit.create(AttendanceApiService::class.java)
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AttendanceViewModel : ViewModel() {
|
||||
|
||||
// ฟังก์ชัน checkIn รับ token, พิกัด lat,long และ callback สำหรับผลลัพธ์
|
||||
fun checkIn(token: String, lat: Double, long: Double, callback: (Boolean, String) -> 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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<LoginResponse> ที่มี token หรือข้อมูลอื่นใน response
|
||||
@POST("/auth/login")
|
||||
suspend fun login(@Body request: LoginRequest): Response<LoginResponse>
|
||||
}
|
||||
|
||||
//suspend คือ คีย์เวิร์ดในภาษา Kotlin ที่ใช้สำหรับระบุว่า ฟังก์ชันสามารถหยุดทำงานชั่วคราว (suspend) และ resume ต่อได้ภายหลัง ซึ่งเป็นพื้นฐานของ Coroutine เพื่อให้สามารถทำงานแบบ asynchronous (ไม่บล็อก UI) ได้ง่ายและปลอดภัยกว่าแบบเดิม (เช่น AsyncTask หรือ Thread)
|
||||
@ -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 สำเร็จ
|
||||
|
||||
// จำลองการเรียก API ที่ใช้เวลาประมาณ 2 วินาที
|
||||
delay(2000)
|
||||
|
||||
|
||||
|
||||
// ตัวอย่าง login จำลอง: email = admin@example.com, password = 1234
|
||||
if (email == "test@test.com" && password == "1234") {
|
||||
callback(true, null)
|
||||
} else {
|
||||
callback(false, "ไม่พบ token")
|
||||
}
|
||||
} else {
|
||||
callback(false, "อีเมลหรือรหัสผ่านไม่ถูกต้อง")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
callback(false, "เกิดข้อผิดพลาดในการเชื่อมต่อ: ${e.localizedMessage}")
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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}")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
// ใช้ Accompanist ในการจัดการ permission สำหรับ location
|
||||
val locationPermissionState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
|
||||
// Context ของแอป สำหรับแสดง Toast หรือใช้ API อื่น ๆ//
|
||||
val ctx = LocalContext.current
|
||||
|
||||
// ใช้ remember เก็บข้อความที่ใช้แสดงผลลัพธ์จาก server
|
||||
val message = remember { mutableStateOf<String?>(null) }
|
||||
|
||||
// ดึง AttendanceViewModel เพื่อเรียกใช้ฟังก์ชัน checkIn
|
||||
val viewModel: AttendanceViewModel = viewModel()
|
||||
|
||||
// token จำลองไว้เรียก API
|
||||
val token = "fake-jwt-token"
|
||||
|
||||
// สร้าง fusedLocationClient สำหรับเข้าถึง location ล่าสุดของผู้ใช้
|
||||
val fusedLocationClient = remember {
|
||||
LocationServices.getFusedLocationProviderClient(ctx)
|
||||
}
|
||||
|
||||
/**
|
||||
* ฟังก์ชันเรียก 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")
|
||||
// แสดงหัวข้อของหน้าจอ
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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" }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user