feat: filter out deleted fields in ListDetailScreen and webRoutes for improved data integrity
This commit is contained in:
@@ -69,11 +69,12 @@ data class Field(
|
|||||||
val isDeleted: Boolean = false,
|
val isDeleted: Boolean = false,
|
||||||
) {
|
) {
|
||||||
fun getType(): FieldType {
|
fun getType(): FieldType {
|
||||||
|
val raw = fieldType.ifBlank { "TEXT" }.uppercase()
|
||||||
return try {
|
return try {
|
||||||
FieldType.valueOf(fieldType)
|
FieldType.valueOf(raw)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Handle legacy field types
|
// Handle legacy/synonym field types
|
||||||
when (fieldType) {
|
when (raw) {
|
||||||
"STRING" -> FieldType.TEXT
|
"STRING" -> FieldType.TEXT
|
||||||
"PRICE" -> FieldType.CURRENCY
|
"PRICE" -> FieldType.CURRENCY
|
||||||
"AMOUNT" -> FieldType.NUMBER
|
"AMOUNT" -> FieldType.NUMBER
|
||||||
@@ -84,7 +85,7 @@ data class Field(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getDropdownOptions(): List<String> {
|
fun getDropdownOptions(): List<String> {
|
||||||
return if (fieldType == "DROPDOWN" && fieldOptions.isNotBlank()) {
|
return if (getType() == FieldType.DROPDOWN && fieldOptions.isNotBlank()) {
|
||||||
fieldOptions.split("|")
|
fieldOptions.split("|")
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
@@ -92,7 +93,7 @@ data class Field(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrency(): String {
|
fun getCurrency(): String {
|
||||||
return if ((fieldType == "CURRENCY" || fieldType == "PRICE") && fieldOptions.isNotBlank()) {
|
return if ((getType() == FieldType.CURRENCY) && fieldOptions.isNotBlank()) {
|
||||||
fieldOptions
|
fieldOptions
|
||||||
} else {
|
} else {
|
||||||
"CHF"
|
"CHF"
|
||||||
@@ -100,7 +101,7 @@ data class Field(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getMaxRating(): Int {
|
fun getMaxRating(): Int {
|
||||||
return if (fieldType == "RATING" && fieldOptions.isNotBlank()) {
|
return if (getType() == FieldType.RATING && fieldOptions.isNotBlank()) {
|
||||||
fieldOptions.toIntOrNull() ?: 5
|
fieldOptions.toIntOrNull() ?: 5
|
||||||
} else {
|
} else {
|
||||||
5
|
5
|
||||||
@@ -108,7 +109,7 @@ data class Field(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getAutocompleteOptions(): List<String> {
|
fun getAutocompleteOptions(): List<String> {
|
||||||
return if (fieldType == "AUTOCOMPLETE" && fieldOptions.isNotBlank()) {
|
return if (getType() == FieldType.AUTOCOMPLETE && fieldOptions.isNotBlank()) {
|
||||||
fieldOptions.split("|")
|
fieldOptions.split("|")
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
|
|||||||
+28
-6
@@ -1345,13 +1345,23 @@ fun AddFieldDialog(
|
|||||||
"CURRENCY" -> "Currency"
|
"CURRENCY" -> "Currency"
|
||||||
"PERCENTAGE" -> "Percentage"
|
"PERCENTAGE" -> "Percentage"
|
||||||
"DROPDOWN" -> "Dropdown"
|
"DROPDOWN" -> "Dropdown"
|
||||||
|
"AUTOCOMPLETE" -> "Autocomplete"
|
||||||
"CHECKBOX" -> "Checkbox"
|
"CHECKBOX" -> "Checkbox"
|
||||||
|
"SWITCH" -> "Switch"
|
||||||
"URL" -> "URL"
|
"URL" -> "URL"
|
||||||
"EMAIL" -> "Email"
|
"EMAIL" -> "Email"
|
||||||
"PHONE" -> "Phone"
|
"PHONE" -> "Phone"
|
||||||
"DATE" -> "Date"
|
"DATE" -> "Date"
|
||||||
"TIME" -> "Time"
|
"TIME" -> "Time"
|
||||||
"DATETIME" -> "Date & Time"
|
"DATETIME" -> "Date & Time"
|
||||||
|
"DURATION" -> "Duration"
|
||||||
|
"IMAGE" -> "Image"
|
||||||
|
"FILE" -> "File"
|
||||||
|
"BARCODE" -> "Barcode"
|
||||||
|
"SIGNATURE" -> "Signature"
|
||||||
|
"RATING" -> "Rating"
|
||||||
|
"COLOR" -> "Color"
|
||||||
|
"LOCATION" -> "Location"
|
||||||
else -> "Text"
|
else -> "Text"
|
||||||
},
|
},
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
@@ -1599,8 +1609,9 @@ fun AddFieldDialog(
|
|||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (fieldName.isNotBlank()) {
|
if (fieldName.isNotBlank()) {
|
||||||
|
val normalizedType = selectedFieldType.uppercase()
|
||||||
val options =
|
val options =
|
||||||
when (selectedFieldType) {
|
when (normalizedType) {
|
||||||
"CURRENCY" -> currency.trim()
|
"CURRENCY" -> currency.trim()
|
||||||
"DROPDOWN" ->
|
"DROPDOWN" ->
|
||||||
dropdownOptions.split(",")
|
dropdownOptions.split(",")
|
||||||
@@ -1615,12 +1626,12 @@ fun AddFieldDialog(
|
|||||||
"RATING" -> currency.trim().ifBlank { "5" }
|
"RATING" -> currency.trim().ifBlank { "5" }
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
onAdd(fieldName.trim(), selectedFieldType, options)
|
onAdd(fieldName.trim(), normalizedType, options)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled =
|
enabled =
|
||||||
fieldName.isNotBlank() &&
|
fieldName.isNotBlank() &&
|
||||||
(selectedFieldType !in listOf("DROPDOWN", "AUTOCOMPLETE") || dropdownOptions.isNotBlank()),
|
(selectedFieldType.uppercase() !in listOf("DROPDOWN", "AUTOCOMPLETE") || dropdownOptions.isNotBlank()),
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.add))
|
Text(stringResource(R.string.add))
|
||||||
}
|
}
|
||||||
@@ -1641,7 +1652,7 @@ fun EditFieldDialog(
|
|||||||
onUpdate: (String, String, String) -> Unit,
|
onUpdate: (String, String, String) -> Unit,
|
||||||
) {
|
) {
|
||||||
var name by remember { mutableStateOf(field.name) }
|
var name by remember { mutableStateOf(field.name) }
|
||||||
var selectedFieldType by remember { mutableStateOf(field.fieldType ?: "TEXT") }
|
var selectedFieldType by remember { mutableStateOf(field.fieldType?.uppercase() ?: "TEXT") }
|
||||||
var dropdownOptions by remember { mutableStateOf(field.getDropdownOptions().joinToString(", ")) }
|
var dropdownOptions by remember { mutableStateOf(field.getDropdownOptions().joinToString(", ")) }
|
||||||
var currency by remember { mutableStateOf(field.getCurrency()) }
|
var currency by remember { mutableStateOf(field.getCurrency()) }
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
@@ -1676,13 +1687,23 @@ fun EditFieldDialog(
|
|||||||
"CURRENCY", "PRICE" -> "Currency"
|
"CURRENCY", "PRICE" -> "Currency"
|
||||||
"PERCENTAGE" -> "Percentage"
|
"PERCENTAGE" -> "Percentage"
|
||||||
"DROPDOWN" -> "Dropdown"
|
"DROPDOWN" -> "Dropdown"
|
||||||
|
"AUTOCOMPLETE" -> "Autocomplete"
|
||||||
"CHECKBOX" -> "Checkbox"
|
"CHECKBOX" -> "Checkbox"
|
||||||
|
"SWITCH" -> "Switch"
|
||||||
"URL" -> "URL"
|
"URL" -> "URL"
|
||||||
"EMAIL" -> "Email"
|
"EMAIL" -> "Email"
|
||||||
"PHONE" -> "Phone"
|
"PHONE" -> "Phone"
|
||||||
"DATE" -> "Date"
|
"DATE" -> "Date"
|
||||||
"TIME" -> "Time"
|
"TIME" -> "Time"
|
||||||
"DATETIME" -> "Date & Time"
|
"DATETIME" -> "Date & Time"
|
||||||
|
"DURATION" -> "Duration"
|
||||||
|
"IMAGE" -> "Image"
|
||||||
|
"FILE" -> "File"
|
||||||
|
"BARCODE" -> "Barcode"
|
||||||
|
"SIGNATURE" -> "Signature"
|
||||||
|
"RATING" -> "Rating"
|
||||||
|
"COLOR" -> "Color"
|
||||||
|
"LOCATION" -> "Location"
|
||||||
else -> "Text"
|
else -> "Text"
|
||||||
},
|
},
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
@@ -1929,8 +1950,9 @@ fun EditFieldDialog(
|
|||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
val normalizedType = selectedFieldType.uppercase()
|
||||||
val options =
|
val options =
|
||||||
when (selectedFieldType) {
|
when (normalizedType) {
|
||||||
"CURRENCY", "PRICE" -> currency.trim()
|
"CURRENCY", "PRICE" -> currency.trim()
|
||||||
"DROPDOWN" ->
|
"DROPDOWN" ->
|
||||||
dropdownOptions.split(",")
|
dropdownOptions.split(",")
|
||||||
@@ -1945,7 +1967,7 @@ fun EditFieldDialog(
|
|||||||
"RATING" -> currency.trim().ifBlank { "5" }
|
"RATING" -> currency.trim().ifBlank { "5" }
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
onUpdate(name.trim(), selectedFieldType, options)
|
onUpdate(name.trim(), normalizedType, options)
|
||||||
},
|
},
|
||||||
enabled = name.isNotBlank() && (selectedFieldType !in listOf("DROPDOWN", "AUTOCOMPLETE") || dropdownOptions.isNotBlank()),
|
enabled = name.isNotBlank() && (selectedFieldType !in listOf("DROPDOWN", "AUTOCOMPLETE") || dropdownOptions.isNotBlank()),
|
||||||
) {
|
) {
|
||||||
|
|||||||
+57
-19
@@ -130,12 +130,15 @@ class ListDetailViewModel(
|
|||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert atomically to avoid intermediate inconsistent states
|
// Insert atomically to avoid intermediate inconsistent states and bump list.updatedAt
|
||||||
database.withTransaction {
|
database.withTransaction {
|
||||||
database.fieldDao().insertField(newField)
|
database.fieldDao().insertField(newField)
|
||||||
if (newValues.isNotEmpty()) {
|
if (newValues.isNotEmpty()) {
|
||||||
database.itemValueDao().insertValues(newValues)
|
database.itemValueDao().insertValues(newValues)
|
||||||
}
|
}
|
||||||
|
database.listDao().getListById(listId)?.let { l ->
|
||||||
|
database.listDao().updateList(l.copy(updatedAt = timestamp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
performSync()
|
performSync()
|
||||||
@@ -144,7 +147,13 @@ class ListDetailViewModel(
|
|||||||
|
|
||||||
fun deleteField(fieldId: String) {
|
fun deleteField(fieldId: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
database.fieldDao().softDeleteField(fieldId, System.currentTimeMillis())
|
val ts = System.currentTimeMillis()
|
||||||
|
database.withTransaction {
|
||||||
|
database.fieldDao().softDeleteField(fieldId, ts)
|
||||||
|
database.listDao().getListById(listId)?.let { l ->
|
||||||
|
database.listDao().updateList(l.copy(updatedAt = ts))
|
||||||
|
}
|
||||||
|
}
|
||||||
performSync()
|
performSync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,14 +167,20 @@ class ListDetailViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val field = database.fieldDao().getFieldById(fieldId)
|
val field = database.fieldDao().getFieldById(fieldId)
|
||||||
if (field != null) {
|
if (field != null) {
|
||||||
database.fieldDao().updateField(
|
val ts = System.currentTimeMillis()
|
||||||
field.copy(
|
database.withTransaction {
|
||||||
name = name,
|
database.fieldDao().updateField(
|
||||||
fieldType = fieldType,
|
field.copy(
|
||||||
fieldOptions = fieldOptions,
|
name = name,
|
||||||
updatedAt = System.currentTimeMillis(),
|
fieldType = fieldType,
|
||||||
),
|
fieldOptions = fieldOptions,
|
||||||
)
|
updatedAt = ts,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
database.listDao().getListById(listId)?.let { l ->
|
||||||
|
database.listDao().updateList(l.copy(updatedAt = ts))
|
||||||
|
}
|
||||||
|
}
|
||||||
performSync()
|
performSync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,6 +213,9 @@ class ListDetailViewModel(
|
|||||||
if (values.isNotEmpty()) {
|
if (values.isNotEmpty()) {
|
||||||
database.itemValueDao().insertValues(values)
|
database.itemValueDao().insertValues(values)
|
||||||
}
|
}
|
||||||
|
database.listDao().getListById(listId)?.let { l ->
|
||||||
|
database.listDao().updateList(l.copy(updatedAt = timestamp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
performSync()
|
performSync()
|
||||||
}
|
}
|
||||||
@@ -230,6 +248,9 @@ class ListDetailViewModel(
|
|||||||
if (values.isNotEmpty()) {
|
if (values.isNotEmpty()) {
|
||||||
database.itemValueDao().insertValues(values)
|
database.itemValueDao().insertValues(values)
|
||||||
}
|
}
|
||||||
|
database.listDao().getListById(listId)?.let { l ->
|
||||||
|
database.listDao().updateList(l.copy(updatedAt = timestamp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
performSync()
|
performSync()
|
||||||
}
|
}
|
||||||
@@ -242,12 +263,18 @@ class ListDetailViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val itemValue = database.itemValueDao().getValueById(itemValueId)
|
val itemValue = database.itemValueDao().getValueById(itemValueId)
|
||||||
if (itemValue != null) {
|
if (itemValue != null) {
|
||||||
database.itemValueDao().updateValue(
|
val ts = System.currentTimeMillis()
|
||||||
itemValue.copy(
|
database.withTransaction {
|
||||||
value = newValue,
|
database.itemValueDao().updateValue(
|
||||||
updatedAt = System.currentTimeMillis(),
|
itemValue.copy(
|
||||||
),
|
value = newValue,
|
||||||
)
|
updatedAt = ts,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
database.listDao().getListById(listId)?.let { l ->
|
||||||
|
database.listDao().updateList(l.copy(updatedAt = ts))
|
||||||
|
}
|
||||||
|
}
|
||||||
performSync()
|
performSync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,7 +282,13 @@ class ListDetailViewModel(
|
|||||||
|
|
||||||
fun deleteItem(itemId: String) {
|
fun deleteItem(itemId: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
database.itemDao().softDeleteItem(itemId, System.currentTimeMillis())
|
val ts = System.currentTimeMillis()
|
||||||
|
database.withTransaction {
|
||||||
|
database.itemDao().softDeleteItem(itemId, ts)
|
||||||
|
database.listDao().getListById(listId)?.let { l ->
|
||||||
|
database.listDao().updateList(l.copy(updatedAt = ts))
|
||||||
|
}
|
||||||
|
}
|
||||||
performSync()
|
performSync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,8 +296,13 @@ class ListDetailViewModel(
|
|||||||
fun reorderFields(reorderedFields: List<Field>) {
|
fun reorderFields(reorderedFields: List<Field>) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val timestamp = System.currentTimeMillis()
|
val timestamp = System.currentTimeMillis()
|
||||||
// Use DAO-level transaction for clarity
|
// Reorder within a transaction and bump list.updatedAt
|
||||||
database.fieldDao().reorderFieldsInTransaction(reorderedFields, timestamp)
|
database.withTransaction {
|
||||||
|
database.fieldDao().reorderFieldsInTransaction(reorderedFields, timestamp)
|
||||||
|
database.listDao().getListById(listId)?.let { l ->
|
||||||
|
database.listDao().updateList(l.copy(updatedAt = timestamp))
|
||||||
|
}
|
||||||
|
}
|
||||||
performSync()
|
performSync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,7 +262,8 @@ router.get('/', (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = lists.map(list => {
|
container.innerHTML = lists.map(list => {
|
||||||
const listFields = fields.filter(f => f.listId === list.id);
|
// Only show non-deleted fields in tags and columns
|
||||||
|
const listFields = fields.filter(f => f.listId === list.id && !f.isDeleted);
|
||||||
const listItems = items.filter(i => i.listId === list.id);
|
const listItems = items.filter(i => i.listId === list.id);
|
||||||
|
|
||||||
return \`
|
return \`
|
||||||
@@ -282,7 +283,7 @@ router.get('/', (req: Request, res: Response) => {
|
|||||||
<div class="fields-container">
|
<div class="fields-container">
|
||||||
<strong>Fields (\${listFields.length}):</strong><br>
|
<strong>Fields (\${listFields.length}):</strong><br>
|
||||||
\${listFields.length > 0
|
\${listFields.length > 0
|
||||||
? listFields.map(f => \`<span class="field-tag \${f.isDeleted ? 'deleted' : ''}">\${f.name} (\${f.fieldType})</span>\`).join('')
|
? listFields.map(f => \`<span class="field-tag">\${f.name} (\${f.fieldType})</span>\`).join('')
|
||||||
: '<span style="color: #999;">No fields</span>'
|
: '<span style="color: #999;">No fields</span>'
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -343,9 +344,9 @@ router.get('/web/data', async (req: Request, res: Response) => {
|
|||||||
res.setHeader('Pragma', 'no-cache');
|
res.setHeader('Pragma', 'no-cache');
|
||||||
res.setHeader('Expires', '0');
|
res.setHeader('Expires', '0');
|
||||||
|
|
||||||
// Exclude deleted lists and items from web UI
|
// Exclude deleted lists/items/fields from web UI
|
||||||
const lists = await dbAdapter.queryAll('SELECT * FROM lists WHERE isDeleted = 0 ORDER BY updatedAt DESC');
|
const lists = await dbAdapter.queryAll('SELECT * FROM lists WHERE isDeleted = 0 ORDER BY updatedAt DESC');
|
||||||
const fields = await dbAdapter.queryAll('SELECT * FROM fields ORDER BY listId ASC, "order" ASC');
|
const fields = await dbAdapter.queryAll('SELECT * FROM fields WHERE isDeleted = 0 ORDER BY listId ASC, "order" ASC');
|
||||||
const items = await dbAdapter.queryAll('SELECT * FROM items WHERE isDeleted = 0 ORDER BY listId ASC, updatedAt DESC');
|
const items = await dbAdapter.queryAll('SELECT * FROM items WHERE isDeleted = 0 ORDER BY listId ASC, updatedAt DESC');
|
||||||
const values = await dbAdapter.queryAll('SELECT * FROM item_values');
|
const values = await dbAdapter.queryAll('SELECT * FROM item_values');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user