package com.servdiary.mobile.ui import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.servdiary.mobile.data.AppointmentDto import com.servdiary.mobile.data.BookAppointmentRequest import com.servdiary.mobile.data.CheckInRequest import com.servdiary.mobile.data.JobDto import com.servdiary.mobile.data.LocationHelper import com.servdiary.mobile.data.LoginRequest import com.servdiary.mobile.data.ServDiaryApi import com.servdiary.mobile.data.SlotDto import com.servdiary.mobile.data.TokenStore import java.time.LocalDate import java.time.OffsetDateTime import java.time.format.DateTimeFormatter import kotlinx.coroutines.launch @Composable fun LoginScreen( api: ServDiaryApi, tokenStore: TokenStore, onLoggedIn: (role: String?) -> Unit, ) { var email by remember { mutableStateOf("") } var password by remember { mutableStateOf("") } var error by remember { mutableStateOf(null) } var loading by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() Column( modifier = Modifier .fillMaxSize() .padding(24.dp), verticalArrangement = Arrangement.Center, ) { Text("ServDiary", style = MaterialTheme.typography.headlineMedium) Spacer(Modifier.height(16.dp)) OutlinedTextField( value = email, onValueChange = { email = it }, label = { Text("Email") }, modifier = Modifier.fillMaxWidth(), singleLine = true, ) Spacer(Modifier.height(8.dp)) OutlinedTextField( value = password, onValueChange = { password = it }, label = { Text("Password") }, modifier = Modifier.fillMaxWidth(), singleLine = true, ) error?.let { Spacer(Modifier.height(8.dp)) Text(it, color = MaterialTheme.colorScheme.error) } Spacer(Modifier.height(16.dp)) Button( onClick = { scope.launch { loading = true error = null try { val response = api.login( LoginRequest(email.trim(), password, "android-mvp") ) tokenStore.token = response.token tokenStore.teamRole = response.teamRole onLoggedIn(response.teamRole) } catch (e: Exception) { error = e.message ?: "Login failed" } finally { loading = false } } }, enabled = !loading, modifier = Modifier.fillMaxWidth(), ) { if (loading) CircularProgressIndicator() else Text("Sign in") } } } @Composable fun StaffHomeScreen( api: ServDiaryApi, onOpen: (AppointmentDto) -> Unit, onLogout: () -> Unit, ) { var items by remember { mutableStateOf>(emptyList()) } var error by remember { mutableStateOf(null) } LaunchedEffect(Unit) { try { val from = LocalDate.now().minusDays(1).toString() val to = LocalDate.now().plusDays(14).toString() items = api.appointments(from, to).data } catch (e: Exception) { error = e.message } } Column(Modifier.fillMaxSize().padding(16.dp)) { RowHeader("Assigned appointments", onLogout) error?.let { Text(it, color = MaterialTheme.colorScheme.error) } LazyColumn { items(items, key = { it.id }) { appointment -> Column( Modifier .fillMaxWidth() .clickable { onOpen(appointment) } .padding(vertical = 12.dp), ) { Text(appointment.job?.title ?: "Job #${appointment.jobId}", style = MaterialTheme.typography.titleMedium) Text(formatInstant(appointment.startsAt)) Text(appointment.status) } } } } } @Composable fun AppointmentDetailScreen( appointment: AppointmentDto, api: ServDiaryApi, locationHelper: LocationHelper, onBack: () -> Unit, onChanged: (AppointmentDto) -> Unit, ) { var message by remember { mutableStateOf(null) } var loading by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() var current by remember { mutableStateOf(appointment) } Column(Modifier.fillMaxSize().padding(16.dp)) { TextButton(onClick = onBack) { Text("Back") } Text(current.job?.title ?: "Appointment", style = MaterialTheme.typography.headlineSmall) Text(formatInstant(current.startsAt)) Text("Status: ${current.status}") message?.let { Text(it) } Spacer(Modifier.height(16.dp)) Button( onClick = { scope.launch { loading = true message = null try { val (lat, lng) = locationHelper.currentLatLng() val response = api.checkIn(current.jobId, current.id, CheckInRequest(lat, lng)) response.appointment?.let { current = it onChanged(it) } message = "Checked in" } catch (e: Exception) { message = e.message ?: "Check-in failed" } finally { loading = false } } }, enabled = !loading, modifier = Modifier.fillMaxWidth(), ) { Text("Check in") } Spacer(Modifier.height(8.dp)) Button( onClick = { scope.launch { loading = true message = null try { val updated = api.complete(current.jobId, current.id) current = updated onChanged(updated) message = "Completed" } catch (e: Exception) { message = e.message ?: "Complete failed" } finally { loading = false } } }, enabled = !loading, modifier = Modifier.fillMaxWidth(), ) { Text("Complete") } } } @Composable fun CustomerHomeScreen( api: ServDiaryApi, onOpenJob: (JobDto) -> Unit, onLogout: () -> Unit, ) { var jobs by remember { mutableStateOf>(emptyList()) } var error by remember { mutableStateOf(null) } LaunchedEffect(Unit) { try { jobs = api.jobs().data } catch (e: Exception) { error = e.message } } Column(Modifier.fillMaxSize().padding(16.dp)) { RowHeader("Your jobs", onLogout) error?.let { Text(it, color = MaterialTheme.colorScheme.error) } LazyColumn { items(jobs, key = { it.id }) { job -> Text( job.title, modifier = Modifier .fillMaxWidth() .clickable { onOpenJob(job) } .padding(vertical = 12.dp), style = MaterialTheme.typography.titleMedium, ) } } } } @Composable fun BookSlotsScreen( job: JobDto, api: ServDiaryApi, onBack: () -> Unit, ) { var date by remember { mutableStateOf(LocalDate.now().plusDays(1).toString()) } var slots by remember { mutableStateOf>(emptyList()) } var message by remember { mutableStateOf(null) } val scope = rememberCoroutineScope() Column(Modifier.fillMaxSize().padding(16.dp)) { TextButton(onClick = onBack) { Text("Back") } Text(job.title, style = MaterialTheme.typography.headlineSmall) OutlinedTextField( value = date, onValueChange = { date = it }, label = { Text("Date (YYYY-MM-DD)") }, modifier = Modifier.fillMaxWidth(), singleLine = true, ) Button( onClick = { scope.launch { message = null try { slots = api.availableSlots(job.id, date).slots if (slots.isEmpty()) message = "No slots for this date" } catch (e: Exception) { message = e.message } } }, modifier = Modifier.fillMaxWidth(), ) { Text("Load slots") } message?.let { Text(it) } LazyColumn { items(slots) { slot -> TextButton( onClick = { scope.launch { try { api.bookAppointment(job.id, BookAppointmentRequest(slot.startsAt)) message = "Booked ${formatInstant(slot.startsAt)}" } catch (e: Exception) { message = e.message ?: "Booking failed" } } }, modifier = Modifier.fillMaxWidth(), ) { Text(formatInstant(slot.startsAt)) } } } } } @Composable private fun RowHeader(title: String, onLogout: () -> Unit) { Column { Text(title, style = MaterialTheme.typography.headlineSmall) TextButton(onClick = onLogout, modifier = Modifier.align(Alignment.End)) { Text("Log out") } } } private fun formatInstant(value: String): String { return try { OffsetDateTime.parse(value).format(DateTimeFormatter.ofPattern("EEE d MMM HH:mm")) } catch (_: Exception) { value } }