280 lines
14 KiB
Kotlin
280 lines
14 KiB
Kotlin
package com.keeply.pantry.ui
|
|
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.Row
|
|
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.layout.size
|
|
import androidx.compose.foundation.layout.width
|
|
import androidx.compose.foundation.lazy.LazyColumn
|
|
import androidx.compose.foundation.lazy.items
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.outlined.Add
|
|
import androidx.compose.material.icons.outlined.Edit
|
|
import androidx.compose.material.icons.outlined.Groups
|
|
import androidx.compose.material.icons.outlined.Logout
|
|
import androidx.compose.material.icons.outlined.MailOutline
|
|
import androidx.compose.material.icons.outlined.Shield
|
|
import androidx.compose.material3.AlertDialog
|
|
import androidx.compose.material3.Button
|
|
import androidx.compose.material3.CircularProgressIndicator
|
|
import androidx.compose.material3.Icon
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.OutlinedButton
|
|
import androidx.compose.material3.OutlinedTextField
|
|
import androidx.compose.material3.Surface
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.material3.TextButton
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.text.style.TextOverflow
|
|
import androidx.compose.ui.text.input.KeyboardType
|
|
import androidx.compose.ui.unit.dp
|
|
import com.keeply.pantry.KeeplyViewModel
|
|
import com.keeply.pantry.data.Household
|
|
import com.keeply.pantry.data.HouseholdMember
|
|
import com.keeply.pantry.data.formatDate
|
|
import com.keeply.pantry.ui.theme.Ink
|
|
import com.keeply.pantry.ui.theme.KeeplyGreen
|
|
import com.keeply.pantry.ui.theme.KeeplyGreenSoft
|
|
import com.keeply.pantry.ui.theme.Muted
|
|
import com.keeply.pantry.ui.theme.Warning
|
|
import com.keeply.pantry.ui.theme.WarningSoft
|
|
|
|
@Composable
|
|
fun HouseholdsScreen(viewModel: KeeplyViewModel) {
|
|
val households = viewModel.households.data.orEmpty()
|
|
val isSiteAdmin = viewModel.user?.isSiteAdmin == true
|
|
var editing by remember { mutableStateOf<Household?>(null) }
|
|
var editorOpen by remember { mutableStateOf(false) }
|
|
var inviting by remember { mutableStateOf<Household?>(null) }
|
|
var leaving by remember { mutableStateOf<Household?>(null) }
|
|
|
|
LazyColumn(
|
|
Modifier.fillMaxSize().padding(horizontal = 18.dp),
|
|
contentPadding = androidx.compose.foundation.layout.PaddingValues(top = 24.dp, bottom = 80.dp),
|
|
verticalArrangement = Arrangement.spacedBy(15.dp),
|
|
) {
|
|
item {
|
|
PageHeader(
|
|
eyebrow = "Pantries work better together",
|
|
title = "Households",
|
|
description = "Manage shared spaces and the people who keep them running.",
|
|
action = if (isSiteAdmin) {
|
|
{
|
|
Button(onClick = { editing = null; viewModel.clearActionError(); editorOpen = true }) {
|
|
Icon(Icons.Outlined.Add, null, Modifier.size(18.dp)); Spacer(Modifier.width(6.dp)); Text("New")
|
|
}
|
|
}
|
|
} else null,
|
|
)
|
|
}
|
|
if (!isSiteAdmin) {
|
|
item {
|
|
Surface(color = KeeplyGreenSoft, shape = RoundedCornerShape(13.dp), modifier = Modifier.fillMaxWidth()) {
|
|
Row(Modifier.padding(15.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
Icon(Icons.Outlined.Shield, null, tint = KeeplyGreen)
|
|
Spacer(Modifier.width(11.dp))
|
|
Column {
|
|
Text("Your household access is managed safely", fontWeight = FontWeight.SemiBold)
|
|
Text("Household admins can update details and add existing Keeply users.", color = Muted, style = MaterialTheme.typography.bodySmall)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
when {
|
|
viewModel.households.loading -> item { LoadingPane("Loading households…") }
|
|
viewModel.households.error != null -> item {
|
|
EmptyPane("Households are unavailable", viewModel.households.error.orEmpty(), "Try again") { viewModel.loadHouseholds(true) }
|
|
}
|
|
households.isEmpty() -> item {
|
|
EmptyPane(
|
|
"No households yet",
|
|
if (isSiteAdmin) "Create a household, then add the people who share it." else "A site administrator can create a household and add you to it.",
|
|
if (isSiteAdmin) "Create household" else null,
|
|
if (isSiteAdmin) ({ editing = null; editorOpen = true }) else null,
|
|
)
|
|
}
|
|
else -> items(households, key = { it.id }) { household ->
|
|
HouseholdCard(
|
|
household = household,
|
|
canManage = isSiteAdmin || household.isCurrentUserHouseholdAdmin,
|
|
onEdit = { editing = household; viewModel.clearActionError(); editorOpen = true },
|
|
onInvite = { inviting = household; viewModel.clearActionError() },
|
|
onLeave = { leaving = household; viewModel.clearActionError() },
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (editorOpen) {
|
|
HouseholdEditorDialog(
|
|
household = editing,
|
|
working = viewModel.working,
|
|
error = viewModel.actionError,
|
|
onSave = { name, description -> viewModel.saveHousehold(editing, name, description) { editorOpen = false } },
|
|
onDismiss = { editorOpen = false },
|
|
)
|
|
}
|
|
inviting?.let { household ->
|
|
InviteMemberDialog(
|
|
household = household,
|
|
working = viewModel.working,
|
|
error = viewModel.actionError,
|
|
onInvite = { email -> viewModel.inviteMember(household, email) { inviting = null } },
|
|
onDismiss = { inviting = null },
|
|
)
|
|
}
|
|
leaving?.let { household ->
|
|
ConfirmActionDialog(
|
|
title = "Leave this household?",
|
|
message = "You will lose access to ${household.name} and its shared search results.",
|
|
confirmLabel = "Leave household",
|
|
working = viewModel.working,
|
|
onConfirm = { viewModel.leaveHousehold(household) { leaving = null } },
|
|
onDismiss = { leaving = null },
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun HouseholdCard(
|
|
household: Household,
|
|
canManage: Boolean,
|
|
onEdit: () -> Unit,
|
|
onInvite: () -> Unit,
|
|
onLeave: () -> Unit,
|
|
) {
|
|
KeeplyCard {
|
|
Column {
|
|
Row(Modifier.fillMaxWidth().padding(17.dp), verticalAlignment = Alignment.Top) {
|
|
Surface(Modifier.size(48.dp), shape = RoundedCornerShape(13.dp), color = KeeplyGreenSoft) {
|
|
androidx.compose.foundation.layout.Box(contentAlignment = Alignment.Center) { Icon(Icons.Outlined.Groups, null, tint = KeeplyGreen) }
|
|
}
|
|
Spacer(Modifier.width(13.dp))
|
|
Column(Modifier.weight(1f)) {
|
|
SectionEyebrow("Established ${formatDate(household.createdAt)}")
|
|
Spacer(Modifier.height(4.dp))
|
|
Text(household.name, style = MaterialTheme.typography.titleLarge, color = Ink)
|
|
Text(household.description ?: "A shared place for a well-kept kitchen.", color = Muted, style = MaterialTheme.typography.bodySmall)
|
|
}
|
|
if (household.isCurrentUserHouseholdAdmin) {
|
|
Surface(color = WarningSoft, contentColor = Warning, shape = RoundedCornerShape(50)) {
|
|
Text("You manage this", Modifier.padding(horizontal = 8.dp, vertical = 5.dp), style = MaterialTheme.typography.labelSmall)
|
|
}
|
|
}
|
|
}
|
|
FieldDivider()
|
|
Row(Modifier.fillMaxWidth().padding(horizontal = 17.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
Text("${household.members.size} ${if (household.members.size == 1) "member" else "members"}", fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
|
if (canManage) TextButton(onClick = onInvite) { Icon(Icons.Outlined.MailOutline, null, Modifier.size(17.dp)); Spacer(Modifier.width(5.dp)); Text("Add member") }
|
|
}
|
|
Column(Modifier.padding(horizontal = 17.dp), verticalArrangement = Arrangement.spacedBy(7.dp)) {
|
|
if (household.members.isEmpty()) Text("No members have been added yet.", color = Muted, style = MaterialTheme.typography.bodySmall)
|
|
household.members.forEach { MemberRow(it) }
|
|
}
|
|
Spacer(Modifier.height(13.dp))
|
|
FieldDivider()
|
|
Row(Modifier.fillMaxWidth().padding(horizontal = 17.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
Text("Managed by ${household.adminEmail}", color = Muted, style = MaterialTheme.typography.bodySmall, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f))
|
|
if (canManage) OutlinedButton(onClick = onEdit) {
|
|
Icon(Icons.Outlined.Edit, null, Modifier.size(16.dp)); Spacer(Modifier.width(5.dp)); Text("Edit")
|
|
}
|
|
if (!household.isCurrentUserHouseholdAdmin) TextButton(onClick = onLeave) {
|
|
Icon(Icons.Outlined.Logout, null, Modifier.size(16.dp)); Spacer(Modifier.width(5.dp)); Text("Leave")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun MemberRow(member: HouseholdMember) {
|
|
Surface(color = Color(0xFFFBFCFA), shape = RoundedCornerShape(11.dp), modifier = Modifier.fillMaxWidth()) {
|
|
Row(Modifier.padding(10.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
InitialsAvatar(member.initials)
|
|
Spacer(Modifier.width(10.dp))
|
|
Column(Modifier.weight(1f)) {
|
|
Text(member.displayName, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
|
Text(member.email, color = Muted, style = MaterialTheme.typography.bodySmall, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
|
}
|
|
if (member.isHouseholdAdmin) Text("Household admin", color = Warning, style = MaterialTheme.typography.labelSmall)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun HouseholdEditorDialog(
|
|
household: Household?,
|
|
working: Boolean,
|
|
error: String?,
|
|
onSave: (String, String?) -> Unit,
|
|
onDismiss: () -> Unit,
|
|
) {
|
|
var name by remember(household?.id) { mutableStateOf(household?.name.orEmpty()) }
|
|
var description by remember(household?.id) { mutableStateOf(household?.description.orEmpty()) }
|
|
AlertDialog(
|
|
onDismissRequest = { if (!working) onDismiss() },
|
|
title = { Column { SectionEyebrow("Shared pantry"); Spacer(Modifier.height(4.dp)); Text(if (household == null) "Create a household" else "Edit household") } },
|
|
text = {
|
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
|
OutlinedTextField(name, { name = it }, label = { Text("Household name") }, placeholder = { Text("e.g. The Morgan household") }, singleLine = true, modifier = Modifier.fillMaxWidth())
|
|
OutlinedTextField(description, { description = it }, label = { Text("Description (optional)") }, minLines = 3, modifier = Modifier.fillMaxWidth())
|
|
ErrorText(error)
|
|
}
|
|
},
|
|
confirmButton = {
|
|
Button(onClick = { onSave(name.trim(), description.trim().ifBlank { null }) }, enabled = !working && name.isNotBlank()) {
|
|
if (working) CircularProgressIndicator(Modifier.size(17.dp), color = Color.White, strokeWidth = 2.dp)
|
|
else Text(if (household == null) "Create household" else "Save changes")
|
|
}
|
|
},
|
|
dismissButton = { TextButton(onClick = onDismiss, enabled = !working) { Text("Cancel") } },
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
private fun InviteMemberDialog(
|
|
household: Household,
|
|
working: Boolean,
|
|
error: String?,
|
|
onInvite: (String) -> Unit,
|
|
onDismiss: () -> Unit,
|
|
) {
|
|
var email by remember(household.id) { mutableStateOf("") }
|
|
AlertDialog(
|
|
onDismissRequest = { if (!working) onDismiss() },
|
|
title = { Column { SectionEyebrow(household.name); Spacer(Modifier.height(4.dp)); Text("Add a household member") } },
|
|
text = {
|
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
|
Text("Invite an existing Keeply user using the email address they use to sign in.", color = Muted)
|
|
OutlinedTextField(
|
|
email, { email = it }, label = { Text("Email address") }, singleLine = true,
|
|
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(keyboardType = KeyboardType.Email),
|
|
modifier = Modifier.fillMaxWidth(),
|
|
)
|
|
ErrorText(error)
|
|
}
|
|
},
|
|
confirmButton = {
|
|
Button(onClick = { onInvite(email.trim()) }, enabled = !working && email.isNotBlank()) {
|
|
if (working) CircularProgressIndicator(Modifier.size(17.dp), color = Color.White, strokeWidth = 2.dp) else Text("Add member")
|
|
}
|
|
},
|
|
dismissButton = { TextButton(onClick = onDismiss, enabled = !working) { Text("Cancel") } },
|
|
)
|
|
}
|