package `in`.carecollect.field.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp /** * The collection screen — the one the phlebotomist actually uses, standing at * a door, often one-handed, sometimes in poor light. * * Design decisions that come from that situation, not from taste: * * - The door code is its own step and cannot be skipped. It is the proof the * right person was visited, and the server refuses a collection without it. * - Vials are shown by tube colour and container, grouped by sample type, not * as a list of test names. The person is packing tubes. * - Every button writes locally and returns. Nothing blocks on the network. * - The failure path is as prominent as the success path. "Patient not at * home" is a normal outcome, not an error, and burying it produces false * collections. */ data class Vial( val code: String, val name: String, val container: String, val colourHex: String, val tests: String, val transportTemp: String ) enum class CollectStep { ARRIVED, VERIFY, VIALS, PAYMENT, DONE } @OptIn(ExperimentalMaterial3Api::class) @Composable fun CollectScreen( patientName: String, address: String, amountDue: Double, fasting: Boolean, vials: List, onVerifyOtp: (String) -> Unit, onCollect: (List>) -> Unit, onPayment: (Double, String) -> Unit, onFail: () -> Unit, otpVerified: Boolean, pendingUploads: Int ) { var step by remember { mutableStateOf(if (otpVerified) CollectStep.VIALS else CollectStep.VERIFY) } var otp by remember { mutableStateOf("") } val counts = remember { mutableStateMapOf().apply { vials.forEach { put(it.code, 1) } } } Scaffold( topBar = { Column { TopAppBar(title = { Column { Text(patientName, fontWeight = FontWeight.SemiBold) Text(address, fontSize = 12.sp, maxLines = 1) } }) // Offline is a normal state in this job, so it is stated // plainly rather than dressed up as an error. if (pendingUploads > 0) { Surface(color = MaterialTheme.colorScheme.tertiaryContainer) { Text( "$pendingUploads action(s) saved on this phone, waiting for signal", modifier = Modifier.fillMaxWidth().padding(10.dp), fontSize = 12.sp ) } } if (fasting) { Surface(color = MaterialTheme.colorScheme.errorContainer) { Text( "Fasting sample — confirm the patient has not eaten", modifier = Modifier.fillMaxWidth().padding(10.dp), fontSize = 13.sp, fontWeight = FontWeight.Medium ) } } } } ) { padding -> Column(Modifier.padding(padding).padding(16.dp).fillMaxSize()) { when (step) { CollectStep.VERIFY -> { Text("Ask the patient for the 6-digit code", fontWeight = FontWeight.SemiBold) Text( "It is in the message they received. Without it the collection cannot start.", fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 4.dp, bottom = 16.dp) ) OutlinedTextField( value = otp, onValueChange = { if (it.length <= 6 && it.all(Char::isDigit)) otp = it }, label = { Text("Door code") }, singleLine = true, textStyle = androidx.compose.ui.text.TextStyle( fontFamily = FontFamily.Monospace, fontSize = 26.sp, letterSpacing = 6.sp ), keyboardOptions = androidx.compose.foundation.text.KeyboardOptions( keyboardType = KeyboardType.NumberPassword ), modifier = Modifier.fillMaxWidth() ) Spacer(Modifier.height(16.dp)) Button( onClick = { onVerifyOtp(otp); step = CollectStep.VIALS }, enabled = otp.length == 6, modifier = Modifier.fillMaxWidth().height(52.dp) ) { Text("Verify and start") } } CollectStep.VIALS -> { Text("Vials to draw", fontWeight = FontWeight.SemiBold) Spacer(Modifier.height(10.dp)) LazyColumn(Modifier.weight(1f)) { items(vials) { v -> VialRow( vial = v, count = counts[v.code] ?: 1, onChange = { counts[v.code] = it } ) } } Button( onClick = { onCollect(counts.filter { it.value > 0 }.map { it.key to it.value }) step = if (amountDue > 0) CollectStep.PAYMENT else CollectStep.DONE }, modifier = Modifier.fillMaxWidth().height(52.dp) ) { Text("Collected — print labels") } } CollectStep.PAYMENT -> { Text("Collect ₹%.0f".format(amountDue), fontWeight = FontWeight.SemiBold, fontSize = 20.sp) Spacer(Modifier.height(16.dp)) Button( onClick = { onPayment(amountDue, "cash"); step = CollectStep.DONE }, modifier = Modifier.fillMaxWidth().height(52.dp) ) { Text("Cash received") } Spacer(Modifier.height(8.dp)) OutlinedButton( onClick = { onPayment(amountDue, "upi"); step = CollectStep.DONE }, modifier = Modifier.fillMaxWidth().height(52.dp) ) { Text("Show UPI QR") } Spacer(Modifier.height(8.dp)) TextButton(onClick = { step = CollectStep.DONE }, modifier = Modifier.fillMaxWidth()) { Text("Patient will pay later") } } CollectStep.DONE -> { Text("Done here", fontWeight = FontWeight.SemiBold, fontSize = 20.sp) Text( "Labels printed. Keep the samples with you until handover at the lab.", fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 6.dp) ) } else -> Unit } if (step != CollectStep.DONE) { Spacer(Modifier.height(12.dp)) // Not hidden behind a menu. An honest "could not collect" is // worth far more than a collection that never happened. TextButton(onClick = onFail, modifier = Modifier.fillMaxWidth()) { Text("Could not collect", color = MaterialTheme.colorScheme.error) } } } } } @Composable private fun VialRow(vial: Vial, count: Int, onChange: (Int) -> Unit) { Card(Modifier.fillMaxWidth().padding(vertical = 5.dp)) { Row( Modifier.padding(12.dp).fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { // The cap colour is what the person matches against the tube in // their hand — it is the fastest identifier in the whole screen. Box( Modifier .size(22.dp) .background( runCatching { Color(android.graphics.Color.parseColor(vial.colourHex)) } .getOrDefault(Color.Gray), CircleShape ) ) Spacer(Modifier.width(12.dp)) Column(Modifier.weight(1f)) { Text(vial.container, fontWeight = FontWeight.Medium) Text( vial.tests, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 2 ) if (vial.transportTemp != "ambient") { Text( "Keep cold", fontSize = 11.sp, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Medium ) } } IconButton(onClick = { if (count > 0) onChange(count - 1) }) { Text("−", fontSize = 22.sp) } Text("$count", fontFamily = FontFamily.Monospace, fontSize = 17.sp) IconButton(onClick = { onChange(count + 1) }) { Text("+", fontSize = 20.sp) } } } }