diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/model/Field.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/model/Field.kt index b775dbb..69ed842 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/model/Field.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/model/Field.kt @@ -6,13 +6,42 @@ import androidx.room.Index import androidx.room.PrimaryKey enum class FieldType { - STRING, - PRICE, + // Text types + TEXT, + MULTILINE_TEXT, + + // Number types + NUMBER, + CURRENCY, + PERCENTAGE, + + // Selection types DROPDOWN, + AUTOCOMPLETE, + CHECKBOX, + SWITCH, + + // Link types URL, + EMAIL, + PHONE, + + // Date/Time types DATE, TIME, - DATETIME + DATETIME, + DURATION, + + // Media types + IMAGE, + FILE, + BARCODE, + SIGNATURE, + + // Other types + RATING, + COLOR, + LOCATION } @Entity( @@ -31,8 +60,8 @@ data class Field( @PrimaryKey val id: String, val listId: String, val name: String, - val fieldType: String = "STRING", // STRING, PRICE, DROPDOWN - val fieldOptions: String = "", // JSON string for dropdown options or currency for price + val fieldType: String = "TEXT", + val fieldOptions: String = "", // JSON string for dropdown options, currency symbol, etc. val order: Int, val createdAt: Long, val updatedAt: Long, @@ -42,7 +71,14 @@ data class Field( return try { FieldType.valueOf(fieldType) } catch (e: Exception) { - FieldType.STRING + // Handle legacy field types + when (fieldType) { + "STRING" -> FieldType.TEXT + "PRICE" -> FieldType.CURRENCY + "AMOUNT" -> FieldType.NUMBER + "SIZE" -> FieldType.TEXT + else -> FieldType.TEXT + } } } @@ -55,10 +91,26 @@ data class Field( } fun getCurrency(): String { - return if (fieldType == "PRICE" && fieldOptions.isNotBlank()) { + return if ((fieldType == "CURRENCY" || fieldType == "PRICE") && fieldOptions.isNotBlank()) { fieldOptions } else { - "$" + "CHF" + } + } + + fun getMaxRating(): Int { + return if (fieldType == "RATING" && fieldOptions.isNotBlank()) { + fieldOptions.toIntOrNull() ?: 5 + } else { + 5 + } + } + + fun getAutocompleteOptions(): List { + return if (fieldType == "AUTOCOMPLETE" && fieldOptions.isNotBlank()) { + fieldOptions.split("|") + } else { + emptyList() } } } diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt index e899ef4..5d9dc07 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/repository/SyncRepository.kt @@ -57,15 +57,29 @@ class SyncRepository(context: Context) { // Log details of lists being received syncResponse.lists.forEach { list -> - Logger.d("Sync", " Inserting list: ${list.id} - ${list.name} (updated: ${list.updatedAt})") + Logger.d("Sync", " Received list: ${list.id} - ${list.name} (updated: ${list.updatedAt}, deleted: ${list.isDeleted})") } // Apply server changes to local database + Logger.d("Sync", "Inserting ${syncResponse.lists.size} lists into local database") database.listDao().insertLists(syncResponse.lists) + + Logger.d("Sync", "Inserting ${syncResponse.fields.size} fields into local database") database.fieldDao().insertFields(syncResponse.fields) + + Logger.d("Sync", "Inserting ${syncResponse.items.size} items into local database") database.itemDao().insertItems(syncResponse.items) + + Logger.d("Sync", "Inserting ${syncResponse.itemValues.size} item values into local database") database.itemValueDao().insertValues(syncResponse.itemValues) + // Verify what was actually saved + val savedLists = database.listDao().getListsUpdatedSince(0) + Logger.d("Sync", "After insert, total lists in DB: ${savedLists.size}") + savedLists.forEach { list -> + Logger.d("Sync", " DB list: ${list.id} - ${list.name} (deleted: ${list.isDeleted})") + } + // Update last sync timestamp setLastSyncTimestamp(syncResponse.serverTimestamp) diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt index 6a43bb4..9b45ee0 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.ClickableText +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* @@ -35,6 +36,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.Dp @@ -605,7 +607,7 @@ fun ItemRow( .padding(8.dp) ) { when (field.getType()) { - com.collabtable.app.data.model.FieldType.STRING -> { + com.collabtable.app.data.model.FieldType.TEXT -> { Text( text = value?.value ?: "", style = MaterialTheme.typography.bodyMedium, @@ -615,7 +617,27 @@ fun ItemRow( ) } - com.collabtable.app.data.model.FieldType.PRICE -> { + com.collabtable.app.data.model.FieldType.MULTILINE_TEXT -> { + Text( + text = value?.value ?: "", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + } + + com.collabtable.app.data.model.FieldType.NUMBER -> { + Text( + text = value?.value ?: "", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + } + + com.collabtable.app.data.model.FieldType.CURRENCY -> { Text( text = if (value?.value.isNullOrBlank()) "" else "${field.getCurrency()}${value?.value}", @@ -626,6 +648,16 @@ fun ItemRow( ) } + com.collabtable.app.data.model.FieldType.PERCENTAGE -> { + Text( + text = if (value?.value.isNullOrBlank()) "" else "${value?.value}%", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + } + com.collabtable.app.data.model.FieldType.DROPDOWN -> { Text( text = value?.value ?: "", @@ -636,6 +668,16 @@ fun ItemRow( ) } + com.collabtable.app.data.model.FieldType.CHECKBOX -> { + Text( + text = if (value?.value == "true") "✓" else "", + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + } + com.collabtable.app.data.model.FieldType.URL -> { val uriHandler = LocalUriHandler.current val urlValue = value?.value ?: "" @@ -674,6 +716,82 @@ fun ItemRow( } } + com.collabtable.app.data.model.FieldType.EMAIL -> { + val uriHandler = LocalUriHandler.current + val emailValue = value?.value ?: "" + if (emailValue.isNotBlank()) { + ClickableText( + text = buildAnnotatedString { + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.primary, + textDecoration = TextDecoration.Underline + ) + ) { + append(emailValue) + } + }, + onClick = { + try { + uriHandler.openUri("mailto:$emailValue") + } catch (e: Exception) { + // Handle invalid email + } + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + style = MaterialTheme.typography.bodyMedium + ) + } else { + Text( + text = "", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + } + } + + com.collabtable.app.data.model.FieldType.PHONE -> { + val uriHandler = LocalUriHandler.current + val phoneValue = value?.value ?: "" + if (phoneValue.isNotBlank()) { + ClickableText( + text = buildAnnotatedString { + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.primary, + textDecoration = TextDecoration.Underline + ) + ) { + append(phoneValue) + } + }, + onClick = { + try { + uriHandler.openUri("tel:$phoneValue") + } catch (e: Exception) { + // Handle invalid phone + } + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + style = MaterialTheme.typography.bodyMedium + ) + } else { + Text( + text = "", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + } + } + com.collabtable.app.data.model.FieldType.DATE -> { Text( text = value?.value ?: "", @@ -703,6 +821,182 @@ fun ItemRow( .padding(vertical = 8.dp) ) } + + // New field types + com.collabtable.app.data.model.FieldType.SWITCH -> { + Text( + text = if (value?.value == "true") "ON" else "OFF", + style = MaterialTheme.typography.bodyMedium, + color = if (value?.value == "true") + MaterialTheme.colorScheme.primary + else + MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + } + + com.collabtable.app.data.model.FieldType.AUTOCOMPLETE -> { + Text( + text = value?.value ?: "", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + } + + com.collabtable.app.data.model.FieldType.DURATION -> { + Text( + text = value?.value ?: "", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + } + + com.collabtable.app.data.model.FieldType.RATING -> { + val rating = value?.value?.toIntOrNull() ?: 0 + val maxRating = field.getMaxRating() + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) { + repeat(maxRating) { index -> + Icon( + imageVector = Icons.Default.Star, + contentDescription = null, + tint = if (index < rating) + MaterialTheme.colorScheme.primary + else + MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.size(16.dp) + ) + } + } + } + + com.collabtable.app.data.model.FieldType.COLOR -> { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (value?.value?.isNotBlank() == true) { + Box( + modifier = Modifier + .size(20.dp) + .background( + color = try { + androidx.compose.ui.graphics.Color( + android.graphics.Color.parseColor(value.value) + ) + } catch (e: Exception) { + MaterialTheme.colorScheme.surface + }, + shape = RoundedCornerShape(4.dp) + ) + .border( + 1.dp, + MaterialTheme.colorScheme.outline, + RoundedCornerShape(4.dp) + ) + ) + Spacer(modifier = Modifier.width(8.dp)) + } + Text( + text = value?.value ?: "", + style = MaterialTheme.typography.bodyMedium + ) + } + } + + com.collabtable.app.data.model.FieldType.LOCATION -> { + Text( + text = value?.value ?: "", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + } + + com.collabtable.app.data.model.FieldType.IMAGE -> { + if (value?.value?.isNotBlank() == true) { + Text( + text = "🖼️ ${value.value}", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + } else { + Text( + text = "", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + } + } + + com.collabtable.app.data.model.FieldType.FILE -> { + if (value?.value?.isNotBlank() == true) { + Text( + text = "📎 ${value.value}", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + } else { + Text( + text = "", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + } + } + + com.collabtable.app.data.model.FieldType.BARCODE -> { + Text( + text = value?.value ?: "", + style = MaterialTheme.typography.bodyMedium.copy( + fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace + ), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + } + + com.collabtable.app.data.model.FieldType.SIGNATURE -> { + if (value?.value?.isNotBlank() == true) { + Text( + text = "✍️ Signed", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + } else { + Text( + text = "", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) + } + } } } } @@ -716,7 +1010,7 @@ fun AddFieldDialog( onAdd: (String, String, String) -> Unit ) { var fieldName by remember { mutableStateOf("") } - var selectedFieldType by remember { mutableStateOf("STRING") } + var selectedFieldType by remember { mutableStateOf("TEXT") } var dropdownOptions by remember { mutableStateOf("") } var currency by remember { mutableStateOf("$") } var expanded by remember { mutableStateOf(false) } @@ -743,10 +1037,16 @@ fun AddFieldDialog( ) { OutlinedTextField( value = when(selectedFieldType) { - "STRING" -> "Text" - "PRICE" -> "Price" + "TEXT" -> "Text" + "MULTILINE_TEXT" -> "Multi-line Text" + "NUMBER" -> "Number" + "CURRENCY" -> "Currency" + "PERCENTAGE" -> "Percentage" "DROPDOWN" -> "Dropdown" + "CHECKBOX" -> "Checkbox" "URL" -> "URL" + "EMAIL" -> "Email" + "PHONE" -> "Phone" "DATE" -> "Date" "TIME" -> "Time" "DATETIME" -> "Date & Time" @@ -765,20 +1065,46 @@ fun AddFieldDialog( expanded = expanded, onDismissRequest = { expanded = false } ) { + // Text types DropdownMenuItem( text = { Text("Text") }, onClick = { - selectedFieldType = "STRING" + selectedFieldType = "TEXT" expanded = false } ) DropdownMenuItem( - text = { Text("Price") }, + text = { Text("Multi-line Text") }, onClick = { - selectedFieldType = "PRICE" + selectedFieldType = "MULTILINE_TEXT" expanded = false } ) + + // Number types + DropdownMenuItem( + text = { Text("Number") }, + onClick = { + selectedFieldType = "NUMBER" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Currency") }, + onClick = { + selectedFieldType = "CURRENCY" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Percentage") }, + onClick = { + selectedFieldType = "PERCENTAGE" + expanded = false + } + ) + + // Selection types DropdownMenuItem( text = { Text("Dropdown") }, onClick = { @@ -786,6 +1112,29 @@ fun AddFieldDialog( expanded = false } ) + DropdownMenuItem( + text = { Text("Autocomplete") }, + onClick = { + selectedFieldType = "AUTOCOMPLETE" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Checkbox") }, + onClick = { + selectedFieldType = "CHECKBOX" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Switch") }, + onClick = { + selectedFieldType = "SWITCH" + expanded = false + } + ) + + // Link types DropdownMenuItem( text = { Text("URL") }, onClick = { @@ -793,6 +1142,22 @@ fun AddFieldDialog( expanded = false } ) + DropdownMenuItem( + text = { Text("Email") }, + onClick = { + selectedFieldType = "EMAIL" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Phone") }, + onClick = { + selectedFieldType = "PHONE" + expanded = false + } + ) + + // Date/Time types DropdownMenuItem( text = { Text("Date") }, onClick = { @@ -814,11 +1179,71 @@ fun AddFieldDialog( expanded = false } ) + DropdownMenuItem( + text = { Text("Duration") }, + onClick = { + selectedFieldType = "DURATION" + expanded = false + } + ) + + // Media types + DropdownMenuItem( + text = { Text("Image") }, + onClick = { + selectedFieldType = "IMAGE" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("File") }, + onClick = { + selectedFieldType = "FILE" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Barcode") }, + onClick = { + selectedFieldType = "BARCODE" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Signature") }, + onClick = { + selectedFieldType = "SIGNATURE" + expanded = false + } + ) + + // Other types + DropdownMenuItem( + text = { Text("Rating") }, + onClick = { + selectedFieldType = "RATING" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Color") }, + onClick = { + selectedFieldType = "COLOR" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Location") }, + onClick = { + selectedFieldType = "LOCATION" + expanded = false + } + ) } } - // Price-specific options - if (selectedFieldType == "PRICE") { + // Currency-specific options + if (selectedFieldType == "CURRENCY") { OutlinedTextField( value = currency, onValueChange = { currency = it }, @@ -840,6 +1265,31 @@ fun AddFieldDialog( supportingText = { Text("Enter dropdown choices separated by commas") } ) } + + // Autocomplete-specific options + if (selectedFieldType == "AUTOCOMPLETE") { + OutlinedTextField( + value = dropdownOptions, + onValueChange = { dropdownOptions = it }, + label = { Text("Options (comma-separated)") }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("Option 1, Option 2, Option 3") }, + supportingText = { Text("Enter autocomplete suggestions separated by commas") } + ) + } + + // Rating-specific options + if (selectedFieldType == "RATING") { + OutlinedTextField( + value = currency, + onValueChange = { currency = it }, + label = { Text("Max Rating") }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("5") }, + supportingText = { Text("Maximum number of stars (default: 5)") } + ) + } } }, confirmButton = { @@ -847,18 +1297,23 @@ fun AddFieldDialog( onClick = { if (fieldName.isNotBlank()) { val options = when(selectedFieldType) { - "PRICE" -> currency.trim() + "CURRENCY" -> currency.trim() "DROPDOWN" -> dropdownOptions.split(",") .map { it.trim() } .filter { it.isNotBlank() } .joinToString("|") + "AUTOCOMPLETE" -> dropdownOptions.split(",") + .map { it.trim() } + .filter { it.isNotBlank() } + .joinToString("|") + "RATING" -> currency.trim().ifBlank { "5" } else -> "" } onAdd(fieldName.trim(), selectedFieldType, options) } }, enabled = fieldName.isNotBlank() && - (selectedFieldType != "DROPDOWN" || dropdownOptions.isNotBlank()) + (selectedFieldType !in listOf("DROPDOWN", "AUTOCOMPLETE") || dropdownOptions.isNotBlank()) ) { Text(stringResource(R.string.add)) } @@ -878,7 +1333,7 @@ fun EditFieldDialog( onDismiss: () -> Unit, onUpdate: (String, String) -> Unit ) { - var selectedFieldType by remember { mutableStateOf(field.fieldType ?: "STRING") } + var selectedFieldType by remember { mutableStateOf(field.fieldType ?: "TEXT") } var dropdownOptions by remember { mutableStateOf(field.getDropdownOptions().joinToString(", ")) } var currency by remember { mutableStateOf(field.getCurrency()) } var expanded by remember { mutableStateOf(false) } @@ -897,10 +1352,16 @@ fun EditFieldDialog( ) { OutlinedTextField( value = when(selectedFieldType) { - "STRING" -> "Text" - "PRICE" -> "Price" + "TEXT", "STRING" -> "Text" + "MULTILINE_TEXT" -> "Multi-line Text" + "NUMBER" -> "Number" + "CURRENCY", "PRICE" -> "Currency" + "PERCENTAGE" -> "Percentage" "DROPDOWN" -> "Dropdown" + "CHECKBOX" -> "Checkbox" "URL" -> "URL" + "EMAIL" -> "Email" + "PHONE" -> "Phone" "DATE" -> "Date" "TIME" -> "Time" "DATETIME" -> "Date & Time" @@ -919,20 +1380,46 @@ fun EditFieldDialog( expanded = expanded, onDismissRequest = { expanded = false } ) { + // Text types DropdownMenuItem( text = { Text("Text") }, onClick = { - selectedFieldType = "STRING" + selectedFieldType = "TEXT" expanded = false } ) DropdownMenuItem( - text = { Text("Price") }, + text = { Text("Multi-line Text") }, onClick = { - selectedFieldType = "PRICE" + selectedFieldType = "MULTILINE_TEXT" expanded = false } ) + + // Number types + DropdownMenuItem( + text = { Text("Number") }, + onClick = { + selectedFieldType = "NUMBER" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Currency") }, + onClick = { + selectedFieldType = "CURRENCY" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Percentage") }, + onClick = { + selectedFieldType = "PERCENTAGE" + expanded = false + } + ) + + // Selection types DropdownMenuItem( text = { Text("Dropdown") }, onClick = { @@ -940,6 +1427,29 @@ fun EditFieldDialog( expanded = false } ) + DropdownMenuItem( + text = { Text("Autocomplete") }, + onClick = { + selectedFieldType = "AUTOCOMPLETE" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Checkbox") }, + onClick = { + selectedFieldType = "CHECKBOX" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Switch") }, + onClick = { + selectedFieldType = "SWITCH" + expanded = false + } + ) + + // Link types DropdownMenuItem( text = { Text("URL") }, onClick = { @@ -947,6 +1457,22 @@ fun EditFieldDialog( expanded = false } ) + DropdownMenuItem( + text = { Text("Email") }, + onClick = { + selectedFieldType = "EMAIL" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Phone") }, + onClick = { + selectedFieldType = "PHONE" + expanded = false + } + ) + + // Date/Time types DropdownMenuItem( text = { Text("Date") }, onClick = { @@ -968,11 +1494,71 @@ fun EditFieldDialog( expanded = false } ) + DropdownMenuItem( + text = { Text("Duration") }, + onClick = { + selectedFieldType = "DURATION" + expanded = false + } + ) + + // Media types + DropdownMenuItem( + text = { Text("Image") }, + onClick = { + selectedFieldType = "IMAGE" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("File") }, + onClick = { + selectedFieldType = "FILE" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Barcode") }, + onClick = { + selectedFieldType = "BARCODE" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Signature") }, + onClick = { + selectedFieldType = "SIGNATURE" + expanded = false + } + ) + + // Other types + DropdownMenuItem( + text = { Text("Rating") }, + onClick = { + selectedFieldType = "RATING" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Color") }, + onClick = { + selectedFieldType = "COLOR" + expanded = false + } + ) + DropdownMenuItem( + text = { Text("Location") }, + onClick = { + selectedFieldType = "LOCATION" + expanded = false + } + ) } } - // Price-specific options - if (selectedFieldType == "PRICE") { + // Currency-specific options + if (selectedFieldType == "CURRENCY" || selectedFieldType == "PRICE") { OutlinedTextField( value = currency, onValueChange = { currency = it }, @@ -994,22 +1580,52 @@ fun EditFieldDialog( supportingText = { Text("Enter dropdown choices separated by commas") } ) } + + // Autocomplete-specific options + if (selectedFieldType == "AUTOCOMPLETE") { + OutlinedTextField( + value = dropdownOptions, + onValueChange = { dropdownOptions = it }, + label = { Text("Options (comma-separated)") }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("Option 1, Option 2, Option 3") }, + supportingText = { Text("Enter autocomplete suggestions separated by commas") } + ) + } + + // Rating-specific options + if (selectedFieldType == "RATING") { + OutlinedTextField( + value = currency, + onValueChange = { currency = it }, + label = { Text("Max Rating") }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("5") }, + supportingText = { Text("Maximum number of stars (default: 5)") } + ) + } } }, confirmButton = { TextButton( onClick = { val options = when(selectedFieldType) { - "PRICE" -> currency.trim() + "CURRENCY", "PRICE" -> currency.trim() "DROPDOWN" -> dropdownOptions.split(",") .map { it.trim() } .filter { it.isNotBlank() } .joinToString("|") + "AUTOCOMPLETE" -> dropdownOptions.split(",") + .map { it.trim() } + .filter { it.isNotBlank() } + .joinToString("|") + "RATING" -> currency.trim().ifBlank { "5" } else -> "" } onUpdate(selectedFieldType, options) }, - enabled = selectedFieldType != "DROPDOWN" || dropdownOptions.isNotBlank() + enabled = selectedFieldType !in listOf("DROPDOWN", "AUTOCOMPLETE") || dropdownOptions.isNotBlank() ) { Text("Update") } @@ -1082,7 +1698,8 @@ fun FieldInput( onValueChange: (String) -> Unit ) { when (field.getType()) { - com.collabtable.app.data.model.FieldType.STRING -> { + // Text types + com.collabtable.app.data.model.FieldType.TEXT -> { OutlinedTextField( value = value, onValueChange = onValueChange, @@ -1092,7 +1709,35 @@ fun FieldInput( ) } - com.collabtable.app.data.model.FieldType.PRICE -> { + com.collabtable.app.data.model.FieldType.MULTILINE_TEXT -> { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(field.name) }, + modifier = Modifier.fillMaxWidth(), + singleLine = false, + minLines = 3, + maxLines = 6 + ) + } + + // Number types + com.collabtable.app.data.model.FieldType.NUMBER -> { + OutlinedTextField( + value = value, + onValueChange = { newValue -> + val filtered = newValue.filter { it.isDigit() || it == '.' || it == '-' } + onValueChange(filtered) + }, + label = { Text(field.name) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + placeholder = { Text("0") } + ) + } + + com.collabtable.app.data.model.FieldType.CURRENCY -> { OutlinedTextField( value = value, onValueChange = { newValue -> @@ -1103,10 +1748,50 @@ fun FieldInput( modifier = Modifier.fillMaxWidth(), singleLine = true, leadingIcon = { Text(field.getCurrency()) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), placeholder = { Text("0.00") } ) } + com.collabtable.app.data.model.FieldType.PERCENTAGE -> { + OutlinedTextField( + value = value, + onValueChange = { newValue -> + val filtered = newValue.filter { it.isDigit() || it == '.' } + onValueChange(filtered) + }, + label = { Text(field.name) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + trailingIcon = { Text("%") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + placeholder = { Text("0") } + ) + } + + // Selection types + com.collabtable.app.data.model.FieldType.CHECKBOX -> { + var checked by remember { mutableStateOf(value == "true") } + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = checked, + onCheckedChange = { + checked = it + onValueChange(it.toString()) + } + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = field.name, + style = MaterialTheme.typography.bodyLarge + ) + } + } + com.collabtable.app.data.model.FieldType.DROPDOWN -> { val options = field.getDropdownOptions() var dropdownExpanded by remember { mutableStateOf(false) } @@ -1152,10 +1837,36 @@ fun FieldInput( label = { Text(field.name) }, modifier = Modifier.fillMaxWidth(), singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), placeholder = { Text("https://example.com") } ) } + com.collabtable.app.data.model.FieldType.EMAIL -> { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(field.name) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), + placeholder = { Text("email@example.com") } + ) + } + + com.collabtable.app.data.model.FieldType.PHONE -> { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(field.name) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), + placeholder = { Text("+1 (555) 123-4567") } + ) + } + + // Date/Time types com.collabtable.app.data.model.FieldType.DATE -> { val dateFormat = remember { SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()) } val calendar = remember { Calendar.getInstance() } @@ -1376,6 +2087,183 @@ fun FieldInput( ) } } + + // New field types + com.collabtable.app.data.model.FieldType.SWITCH -> { + var checked by remember { mutableStateOf(value == "true") } + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = field.name, + style = MaterialTheme.typography.bodyLarge + ) + Switch( + checked = checked, + onCheckedChange = { + checked = it + onValueChange(it.toString()) + } + ) + } + } + + com.collabtable.app.data.model.FieldType.AUTOCOMPLETE -> { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(field.name) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + placeholder = { Text("Start typing...") }, + supportingText = { + val options = field.getAutocompleteOptions() + if (options.isNotEmpty()) { + Text("Suggestions: ${options.take(3).joinToString(", ")}") + } else null + } + ) + } + + com.collabtable.app.data.model.FieldType.DURATION -> { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(field.name) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + placeholder = { Text("2:30 (hours:minutes)") }, + supportingText = { Text("Format: HH:MM") } + ) + } + + com.collabtable.app.data.model.FieldType.RATING -> { + val maxRating = field.getMaxRating() + var rating by remember { mutableStateOf(value.toIntOrNull() ?: 0) } + + Column(modifier = Modifier.fillMaxWidth()) { + Text( + text = field.name, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 8.dp) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ) { + for (i in 1..maxRating) { + IconButton( + onClick = { + rating = i + onValueChange(i.toString()) + } + ) { + Icon( + imageVector = if (i <= rating) Icons.Default.Star else Icons.Default.Star, + contentDescription = "Star $i", + tint = if (i <= rating) + MaterialTheme.colorScheme.primary + else + MaterialTheme.colorScheme.outlineVariant + ) + } + } + } + } + } + + com.collabtable.app.data.model.FieldType.COLOR -> { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(field.name) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + placeholder = { Text("#FF5733 or red") }, + leadingIcon = { + // Show color preview if valid + if (value.isNotBlank()) { + Box( + modifier = Modifier + .size(24.dp) + .background( + color = try { + androidx.compose.ui.graphics.Color(android.graphics.Color.parseColor(value)) + } catch (e: Exception) { + MaterialTheme.colorScheme.surface + }, + shape = RoundedCornerShape(4.dp) + ) + .border( + 1.dp, + MaterialTheme.colorScheme.outline, + RoundedCornerShape(4.dp) + ) + ) + } + } + ) + } + + com.collabtable.app.data.model.FieldType.LOCATION -> { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(field.name) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + placeholder = { Text("Address or coordinates") } + ) + } + + com.collabtable.app.data.model.FieldType.IMAGE -> { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(field.name) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + placeholder = { Text("Image URL") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri) + ) + } + + com.collabtable.app.data.model.FieldType.FILE -> { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(field.name) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + placeholder = { Text("File path or URL") } + ) + } + + com.collabtable.app.data.model.FieldType.BARCODE -> { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(field.name) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + placeholder = { Text("Scan or enter barcode") } + ) + } + + com.collabtable.app.data.model.FieldType.SIGNATURE -> { + OutlinedTextField( + value = value, + onValueChange = {}, + readOnly = true, + label = { Text(field.name) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + placeholder = { Text("Tap to sign") } + ) + } } } @@ -1634,13 +2522,42 @@ fun ColumnItem( ) Text( text = when (field.getType()) { - com.collabtable.app.data.model.FieldType.STRING -> "Text" - com.collabtable.app.data.model.FieldType.PRICE -> "Price (${field.getCurrency()})" + // Text types + com.collabtable.app.data.model.FieldType.TEXT -> "Text" + com.collabtable.app.data.model.FieldType.MULTILINE_TEXT -> "Multiline Text" + + // Number types + com.collabtable.app.data.model.FieldType.NUMBER -> "Number" + com.collabtable.app.data.model.FieldType.CURRENCY -> "Currency (${field.getCurrency()})" + com.collabtable.app.data.model.FieldType.PERCENTAGE -> "Percentage" + + // Selection types + com.collabtable.app.data.model.FieldType.CHECKBOX -> "Checkbox" + com.collabtable.app.data.model.FieldType.SWITCH -> "Switch" com.collabtable.app.data.model.FieldType.DROPDOWN -> "Dropdown (${field.getDropdownOptions().size} options)" + com.collabtable.app.data.model.FieldType.AUTOCOMPLETE -> "Autocomplete (${field.getAutocompleteOptions().size} options)" + + // Link types com.collabtable.app.data.model.FieldType.URL -> "URL" + com.collabtable.app.data.model.FieldType.EMAIL -> "Email" + com.collabtable.app.data.model.FieldType.PHONE -> "Phone" + + // Date/Time types com.collabtable.app.data.model.FieldType.DATE -> "Date" com.collabtable.app.data.model.FieldType.TIME -> "Time" com.collabtable.app.data.model.FieldType.DATETIME -> "Date & Time" + com.collabtable.app.data.model.FieldType.DURATION -> "Duration" + + // Media types + com.collabtable.app.data.model.FieldType.IMAGE -> "Image" + com.collabtable.app.data.model.FieldType.FILE -> "File" + com.collabtable.app.data.model.FieldType.BARCODE -> "Barcode" + com.collabtable.app.data.model.FieldType.SIGNATURE -> "Signature" + + // Other types + com.collabtable.app.data.model.FieldType.RATING -> "Rating (${field.getMaxRating()} stars)" + com.collabtable.app.data.model.FieldType.COLOR -> "Color" + com.collabtable.app.data.model.FieldType.LOCATION -> "Location" }, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt index 95e7f7b..c07f2a7 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt @@ -91,6 +91,22 @@ class ListDetailViewModel( updatedAt = timestamp ) database.fieldDao().insertField(newField) + + // Create empty ItemValue entries for this new field for all existing items + val existingItems = _items.value + if (existingItems.isNotEmpty()) { + val newValues = existingItems.map { itemWithValues -> + ItemValue( + id = UUID.randomUUID().toString(), + itemId = itemWithValues.item.id, + fieldId = newField.id, + value = "", + updatedAt = timestamp + ) + } + database.itemValueDao().insertValues(newValues) + } + performSync() } } diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt index 2e68764..131d6a3 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.List import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Settings @@ -39,6 +40,7 @@ fun ListsScreen( var showCreateDialog by remember { mutableStateOf(false) } var listToDelete by remember { mutableStateOf(null) } + var listToEdit by remember { mutableStateOf(null) } Scaffold( topBar = { @@ -94,6 +96,7 @@ fun ListsScreen( ListItem( list = list, onListClick = { onNavigateToList(list.id) }, + onEditClick = { listToEdit = list }, onDeleteClick = { listToDelete = list } ) } @@ -111,6 +114,17 @@ fun ListsScreen( ) } + listToEdit?.let { list -> + RenameListDialog( + currentName = list.name, + onDismiss = { listToEdit = null }, + onRename = { newName -> + viewModel.renameList(list.id, newName) + listToEdit = null + } + ) + } + listToDelete?.let { list -> AlertDialog( onDismissRequest = { listToDelete = null }, @@ -139,6 +153,7 @@ fun ListsScreen( fun ListItem( list: CollabList, onListClick: () -> Unit, + onEditClick: () -> Unit, onDeleteClick: () -> Unit ) { Card( @@ -165,12 +180,21 @@ fun ListItem( color = MaterialTheme.colorScheme.onSurfaceVariant ) } - IconButton(onClick = onDeleteClick) { - Icon( - Icons.Default.Delete, - contentDescription = stringResource(R.string.delete), - tint = MaterialTheme.colorScheme.error - ) + Row { + IconButton(onClick = onEditClick) { + Icon( + Icons.Default.Edit, + contentDescription = "Edit", + tint = MaterialTheme.colorScheme.primary + ) + } + IconButton(onClick = onDeleteClick) { + Icon( + Icons.Default.Delete, + contentDescription = stringResource(R.string.delete), + tint = MaterialTheme.colorScheme.error + ) + } } } } @@ -210,6 +234,41 @@ fun CreateListDialog( ) } +@Composable +private fun RenameListDialog( + currentName: String, + onDismiss: () -> Unit, + onRename: (String) -> Unit +) { + var listName by remember { mutableStateOf(currentName) } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Rename List") }, + text = { + OutlinedTextField( + value = listName, + onValueChange = { listName = it }, + label = { Text(stringResource(R.string.list_name)) }, + singleLine = true + ) + }, + confirmButton = { + TextButton( + onClick = { if (listName.isNotBlank()) onRename(listName.trim()) }, + enabled = listName.isNotBlank() && listName.trim() != currentName + ) { + Text("Rename") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.cancel)) + } + } + ) +} + private fun formatDate(timestamp: Long): String { val sdf = SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.getDefault()) return sdf.format(Date(timestamp)) diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsViewModel.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsViewModel.kt index 6edcf3a..90a77ab 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsViewModel.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsViewModel.kt @@ -69,6 +69,23 @@ class ListsViewModel( } } + fun renameList(listId: String, newName: String) { + viewModelScope.launch { + Logger.i("ListsViewModel", "Renaming list: $listId to $newName") + val list = database.listDao().getListById(listId) + if (list != null && newName.isNotBlank()) { + database.listDao().updateList( + list.copy( + name = newName.trim(), + updatedAt = System.currentTimeMillis() + ) + ) + // Sync immediately after renaming + performSync() + } + } + } + fun deleteList(listId: String) { viewModelScope.launch { Logger.i("ListsViewModel", "Deleting list: $listId") diff --git a/CollabTableAndroid/build-output.txt b/CollabTableAndroid/build-output.txt index 041017a..cea9e63 100644 --- a/CollabTableAndroid/build-output.txt +++ b/CollabTableAndroid/build-output.txt @@ -1,16 +1,57 @@ -Initialized native services in: C:\Users\gabri\.gradle\native -Initialized jansi services in: C:\Users\gabri\.gradle\native -Received JVM installation metadata from 'C:\Programs\Java 22 JDK': {JAVA_HOME=C:\Programs\Java 22 JDK, JAVA_VERSION=22.0.1, JAVA_VENDOR=Oracle Corporation, RUNTIME_NAME=Java(TM) SE Runtime Environment, RUNTIME_VERSION=22.0.1+8-16, VM_NAME=Java HotSpot(TM) 64-Bit Server VM, VM_VERSION=22.0.1+8-16, VM_VENDOR=Oracle Corporation, OS_ARCH=amd64} -Removing 0 daemon stop events from registry -Starting a Gradle Daemon (subsequent builds will be faster) -Starting process 'Gradle build daemon'. Working directory: C:\Users\gabri\.gradle\daemon\8.5 Command: C:\Programs\Java 22 JDK\bin\java.exe --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED -Xmx2048m -Dfile.encoding=UTF-8 -Duser.country=GB -Duser.language=en -Duser.variant -cp C:\Users\gabri\.gradle\wrapper\dists\gradle-8.5-bin\5t9huq95ubn472n8rpzujfbqh\gradle-8.5\lib\gradle-launcher-8.5.jar -javaagent:C:\Users\gabri\.gradle\wrapper\dists\gradle-8.5-bin\5t9huq95ubn472n8rpzujfbqh\gradle-8.5\lib\agents\gradle-instrumentation-agent-8.5.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 8.5 -Successfully started process 'Gradle build daemon' -An attempt to start the daemon took 8.895 secs. -The client will now receive all logging from the daemon (pid: 35040). The daemon log file: C:\Users\gabri\.gradle\daemon\8.5\daemon-35040.out.log -Starting build in new daemon [memory: 2 GiB] -Using 16 worker leases. -Received JVM installation metadata from 'C:\Programs\Java 22 JDK': {JAVA_HOME=C:\Programs\Java 22 JDK, JAVA_VERSION=22.0.1, JAVA_VENDOR=Oracle Corporation, RUNTIME_NAME=Java(TM) SE Runtime Environment, RUNTIME_VERSION=22.0.1+8-16, VM_NAME=Java HotSpot(TM) 64-Bit Server VM, VM_VERSION=22.0.1+8-16, VM_VENDOR=Oracle Corporation, OS_ARCH=amd64} -Watching the file system is configured to be enabled if available -Now considering [C:\Users\gabri\VSCode Projects\CollabTable\CollabTableAndroid] as hierarchies to watch -File system watching is active -Terminate batch job (Y/N)? \ No newline at end of file +> Task :app:preBuild UP-TO-DATE +> Task :app:preDebugBuild UP-TO-DATE +> Task :app:mergeDebugNativeDebugMetadata NO-SOURCE +> Task :app:checkKotlinGradlePluginConfigurationErrors +> Task :app:checkDebugAarMetadata UP-TO-DATE +> Task :app:generateDebugResValues UP-TO-DATE +> Task :app:mapDebugSourceSetPaths UP-TO-DATE +> Task :app:generateDebugResources UP-TO-DATE +> Task :app:mergeDebugResources UP-TO-DATE +> Task :app:packageDebugResources UP-TO-DATE +> Task :app:parseDebugLocalResources UP-TO-DATE +> Task :app:createDebugCompatibleScreenManifests UP-TO-DATE +> Task :app:extractDeepLinksDebug UP-TO-DATE +> Task :app:processDebugMainManifest UP-TO-DATE +> Task :app:processDebugManifest UP-TO-DATE +> Task :app:processDebugManifestForPackage UP-TO-DATE +> Task :app:processDebugResources UP-TO-DATE +> Task :app:javaPreCompileDebug UP-TO-DATE +> Task :app:mergeDebugShaders UP-TO-DATE +> Task :app:compileDebugShaders NO-SOURCE +> Task :app:generateDebugAssets UP-TO-DATE +> Task :app:mergeDebugAssets UP-TO-DATE +> Task :app:compressDebugAssets UP-TO-DATE +> Task :app:desugarDebugFileDependencies UP-TO-DATE +> Task :app:checkDebugDuplicateClasses UP-TO-DATE +> Task :app:mergeExtDexDebug UP-TO-DATE +> Task :app:mergeLibDexDebug UP-TO-DATE +> Task :app:mergeDebugJniLibFolders UP-TO-DATE +> Task :app:mergeDebugNativeLibs NO-SOURCE +> Task :app:stripDebugDebugSymbols NO-SOURCE +> Task :app:validateSigningDebug UP-TO-DATE +> Task :app:writeDebugAppMetadata UP-TO-DATE +> Task :app:writeDebugSigningConfigVersions UP-TO-DATE +> Task :app:kspDebugKotlin + +> Task :app:compileDebugKotlin +w: file:///C:/Users/gabri/VSCode%20Projects/CollabTable/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt:1336:72 Elvis operator (?:) always returns the left operand of non-nullable type String +w: file:///C:/Users/gabri/VSCode%20Projects/CollabTable/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt:2126:28 The expression is unused + +> Task :app:compileDebugJavaWithJavac +> Task :app:dexBuilderDebug +> Task :app:mergeDebugGlobalSynthetics UP-TO-DATE +> Task :app:processDebugJavaRes UP-TO-DATE +> Task :app:mergeDebugJavaResource UP-TO-DATE +> Task :app:mergeProjectDexDebug +> Task :app:packageDebug +> Task :app:createDebugApkListingFileRedirect UP-TO-DATE +> Task :app:assembleDebug + +Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0. + +You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins. + +For more on this, please refer to https://docs.gradle.org/8.10/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation. + +BUILD SUCCESSFUL in 38s +36 actionable tasks: 7 executed, 29 up-to-date diff --git a/CollabTableServer/collabtable.db b/CollabTableServer/collabtable.db new file mode 100644 index 0000000..e69de29 diff --git a/CollabTableServer/data/collabtable.db b/CollabTableServer/data/collabtable.db new file mode 100644 index 0000000..ee45af8 Binary files /dev/null and b/CollabTableServer/data/collabtable.db differ diff --git a/CollabTableServer/src/routes/syncRoutes.ts b/CollabTableServer/src/routes/syncRoutes.ts index 7b0bcb7..9e40528 100644 --- a/CollabTableServer/src/routes/syncRoutes.ts +++ b/CollabTableServer/src/routes/syncRoutes.ts @@ -177,13 +177,14 @@ router.post('/sync', async (req: Request, res: Response) => { transaction({ lastSyncTimestamp, lists, fields, items, itemValues }); - // Get updates from server since last sync + // IMPORTANT: Get updates from server AFTER saving incoming data + // This ensures clients receive back any data they just sent (for confirmation) // If lastSyncTimestamp is 0, get all data (initial sync) // IMPORTANT: Include deleted items so clients can sync deletions let serverLists, serverFields, serverItems, serverItemValues; if (lastSyncTimestamp === 0) { - // Initial sync: send all non-deleted items + // Initial sync: send all non-deleted items (AFTER processing incoming data) serverLists = db.prepare('SELECT * FROM lists WHERE isDeleted = 0').all(); serverFields = db.prepare('SELECT * FROM fields WHERE isDeleted = 0').all(); serverItems = db.prepare('SELECT * FROM items WHERE isDeleted = 0').all();