11 Commits

36 changed files with 1639 additions and 794 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ android {
applicationId "net.helcel.cowspent" applicationId "net.helcel.cowspent"
minSdk = 26 minSdk = 26
targetSdk = 37 targetSdk = 37
versionName project.hasProperty('VERSION_NAME') ? project.property('VERSION_NAME') : "1" versionName project.hasProperty('VERSION_NAME') ? project.property('VERSION_NAME') : "1.4"
versionCode project.hasProperty('VERSION_CODE') ? project.property('VERSION_CODE').toInteger() : 1 versionCode project.hasProperty('VERSION_CODE') ? project.property('VERSION_CODE').toInteger() : 1
} }
@@ -26,7 +26,6 @@ import net.helcel.cowspent.model.ProjectType
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.theme.ThemeUtils import net.helcel.cowspent.theme.ThemeUtils
import net.helcel.cowspent.util.BillParser import net.helcel.cowspent.util.BillParser
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.SupportUtil import net.helcel.cowspent.util.SupportUtil
import java.text.ParseException import java.text.ParseException
import java.time.ZoneId import java.time.ZoneId
@@ -52,23 +51,10 @@ class EditBillActivity : AppCompatActivity() {
ThemeUtils.CowspentTheme { ThemeUtils.CowspentTheme {
val categories = remember { val categories = remember {
val syncedCategories = db.getCategories(bill.projectId) db.getCategories(bill.projectId)
val defaultCategories = CategoryUtils.getDefaultCategories(this@EditBillActivity, bill.projectId)
val hardcoded = if (projectType == ProjectType.LOCAL) {
defaultCategories
} else {
listOfNotNull(defaultCategories.find { it.remoteId.toInt() == DBBill.CATEGORY_REIMBURSEMENT })
}
syncedCategories + hardcoded
} }
val paymentModes = remember { val paymentModes = remember {
val syncedPaymentModes = db.getPaymentModes(bill.projectId) db.getPaymentModes(bill.projectId)
val defaultPaymentModes = CategoryUtils.getDefaultPaymentModes(this@EditBillActivity, bill.projectId)
if (projectType == ProjectType.LOCAL) {
syncedPaymentModes + defaultPaymentModes
} else {
syncedPaymentModes.ifEmpty { defaultPaymentModes }
}
} }
EditBillScreen( EditBillScreen(
@@ -144,7 +130,7 @@ class EditBillActivity : AppCompatActivity() {
bill = DBBill( bill = DBBill(
first.id, 0, first.projectId, first.payerId, totalAmount, first.id, 0, first.projectId, first.payerId, totalAmount,
first.timestamp, first.what, first.state, first.repeat, first.timestamp, first.what, first.state, first.repeat,
first.paymentMode, first.categoryRemoteId, first.comment, first.paymentModeRemoteId first.paymentMode, first.categoryId, first.comment, first.paymentModeId
) )
val splits = mutableMapOf<Long, Double>() val splits = mutableMapOf<Long, Double>()
@@ -175,8 +161,8 @@ class EditBillActivity : AppCompatActivity() {
bill = DBBill( bill = DBBill(
0, 0, projectId, btd.payerId, btd.amount, 0, 0, projectId, btd.payerId, btd.amount,
timeNowSeconds, btd.what, DBBill.STATE_ADDED, timeNowSeconds, btd.what, DBBill.STATE_ADDED,
btd.repeat, btd.paymentMode, btd.categoryRemoteId, btd.repeat, btd.paymentMode, btd.categoryId,
btd.comment, btd.paymentModeRemoteId btd.comment, btd.paymentModeId
) )
val btdOwers = btd.billOwers val btdOwers = btd.billOwers
val newBillOwers = btdOwers.filter { val newBillOwers = btdOwers.filter {
@@ -195,6 +181,11 @@ class EditBillActivity : AppCompatActivity() {
val project = db.getProject(bill.projectId) val project = db.getProject(bill.projectId)
val currencies = db.getCurrencies(bill.projectId) val currencies = db.getCurrencies(bill.projectId)
if (project != null) {
db.ensureDefaultLabels(bill.projectId, project.type)
}
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
viewModel.currencies = currencies viewModel.currencies = currencies
viewModel.mainCurrencyName = project?.currencyName ?: "" viewModel.mainCurrencyName = project?.currencyName ?: ""
@@ -239,8 +230,8 @@ class EditBillActivity : AppCompatActivity() {
bill = DBBill( bill = DBBill(
0, 0, bill.projectId, viewModel.payerId, viewModel.amountAsDouble, 0, 0, bill.projectId, viewModel.payerId, viewModel.amountAsDouble,
System.currentTimeMillis() / 1000, viewModel.what, DBBill.STATE_ADDED, System.currentTimeMillis() / 1000, viewModel.what, DBBill.STATE_ADDED,
viewModel.repeat, bill.paymentMode, viewModel.categoryRemoteId, viewModel.repeat, bill.paymentMode, viewModel.categoryId,
viewModel.getFinalComment(), viewModel.paymentModeRemoteId viewModel.getFinalComment(), viewModel.paymentModeId
) )
calendar.timeInMillis = System.currentTimeMillis() calendar.timeInMillis = System.currentTimeMillis()
viewModel.timestamp = calendar.timeInMillis / 1000 viewModel.timestamp = calendar.timeInMillis / 1000
@@ -326,8 +317,8 @@ class EditBillActivity : AppCompatActivity() {
bill.payerId == viewModel.payerId && bill.payerId == viewModel.payerId &&
bill.comment == viewModel.getFinalComment() && bill.comment == viewModel.getFinalComment() &&
bill.repeat == viewModel.repeat && bill.repeat == viewModel.repeat &&
bill.categoryRemoteId == viewModel.categoryRemoteId && bill.categoryId == viewModel.categoryId &&
bill.paymentModeRemoteId == viewModel.paymentModeRemoteId && bill.paymentModeId == viewModel.paymentModeId &&
!owersChanged) !owersChanged)
} }
@@ -386,8 +377,8 @@ class EditBillActivity : AppCompatActivity() {
listOf(memberId), listOf(memberId),
viewModel.repeat, viewModel.repeat,
existingBill.paymentMode, existingBill.paymentMode,
viewModel.paymentModeRemoteId, viewModel.paymentModeId,
viewModel.categoryRemoteId, viewModel.categoryId,
finalComment finalComment
) )
if (firstSavedId == 0L) firstSavedId = billToUseId if (firstSavedId == 0L) firstSavedId = billToUseId
@@ -396,9 +387,11 @@ class EditBillActivity : AppCompatActivity() {
val newBill = DBBill( val newBill = DBBill(
0, 0, bill.projectId, viewModel.payerId, amount, 0, 0, bill.projectId, viewModel.payerId, amount,
viewModel.timestamp, viewModel.what, DBBill.STATE_ADDED, viewModel.repeat, viewModel.timestamp, viewModel.what, DBBill.STATE_ADDED, viewModel.repeat,
bill.paymentMode, viewModel.categoryRemoteId, finalComment, viewModel.paymentModeRemoteId bill.paymentMode, viewModel.categoryId, finalComment, viewModel.paymentModeId
) )
newBill.billOwers = listOf(DBBillOwer(0, 0, memberId)) newBill.billOwers = listOf(DBBillOwer(0, 0, memberId))
newBill.categoryId = viewModel.categoryId
newBill.paymentModeId = viewModel.paymentModeId
val newId = db.addBill(newBill) val newId = db.addBill(newBill)
if (firstSavedId == 0L) firstSavedId = newId if (firstSavedId == 0L) firstSavedId = newId
} }
@@ -441,8 +434,8 @@ class EditBillActivity : AppCompatActivity() {
newOwersIds, newOwersIds,
viewModel.repeat, viewModel.repeat,
bill.paymentMode, bill.paymentMode,
viewModel.paymentModeRemoteId, viewModel.paymentModeId,
viewModel.categoryRemoteId, viewModel.categoryId,
finalComment finalComment
) )
if (groupedBillIds != null) { if (groupedBillIds != null) {
@@ -460,7 +453,7 @@ class EditBillActivity : AppCompatActivity() {
val newBill = DBBill( val newBill = DBBill(
0, 0, bill.projectId, viewModel.payerId, newAmount, 0, 0, bill.projectId, viewModel.payerId, newAmount,
viewModel.timestamp, viewModel.what, DBBill.STATE_ADDED, viewModel.repeat, viewModel.timestamp, viewModel.what, DBBill.STATE_ADDED, viewModel.repeat,
bill.paymentMode, viewModel.categoryRemoteId, finalComment, viewModel.paymentModeRemoteId bill.paymentMode, viewModel.categoryId, finalComment, viewModel.paymentModeId
) )
newOwersIds.forEach { newBill.billOwers += DBBillOwer(0, 0, it) } newOwersIds.forEach { newBill.billOwers += DBBillOwer(0, 0, it) }
val newBillId = db.addBill(newBill) val newBillId = db.addBill(newBill)
@@ -31,6 +31,7 @@ import androidx.compose.ui.unit.sp
import net.helcel.cowspent.R import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.* import net.helcel.cowspent.android.helper.*
import net.helcel.cowspent.model.* import net.helcel.cowspent.model.*
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.SupportUtil import net.helcel.cowspent.util.SupportUtil
import java.util.Date import java.util.Date
import kotlin.math.abs import kotlin.math.abs
@@ -466,9 +467,10 @@ fun BillAdditionalDetailsSection(
modifier = Modifier.padding(bottom = 8.dp, top = 16.dp) modifier = Modifier.padding(bottom = 8.dp, top = 16.dp)
) )
val context = LocalContext.current
var categoryExpanded by remember { mutableStateOf(false) } var categoryExpanded by remember { mutableStateOf(false) }
val selectedCategory = val selectedCategory =
categories.find { it.remoteId.toInt() == viewModel.categoryRemoteId } categories.find { it.id == viewModel.categoryId } ?: CategoryUtils.getCategoryById(context, viewModel.categoryId)
EditableExposedDropdownMenu( EditableExposedDropdownMenu(
value = selectedCategory?.name ?: "", value = selectedCategory?.name ?: "",
@@ -488,16 +490,26 @@ fun BillAdditionalDetailsSection(
}, },
content = { content = {
DropdownMenuItem(onClick = { DropdownMenuItem(onClick = {
viewModel.categoryRemoteId = 0 viewModel.categoryId = 0
categoryExpanded = false categoryExpanded = false
}) { }) {
Icon(Icons.Default.Close, tint = Color.Red, contentDescription = null) Icon(Icons.Default.Close, tint = Color.Red, contentDescription = null)
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
Text(stringResource(R.string.category_none)) Text(stringResource(R.string.category_none))
} }
categories.forEach { category ->
DropdownMenuItem(onClick = {
viewModel.categoryId = DBBill.CATEGORY_REIMBURSEMENT
categoryExpanded = false
}) {
Text(text = "\uD83D\uDCB0", fontSize = 20.sp)
Spacer(modifier = Modifier.width(12.dp))
Text(stringResource(R.string.category_reimbursement))
}
categories.filter { it.remoteId != DBBill.CATEGORY_REIMBURSEMENT }.forEach { category ->
DropdownMenuItem(onClick = { DropdownMenuItem(onClick = {
viewModel.categoryRemoteId = category.remoteId.toInt() viewModel.categoryId = category.id
categoryExpanded = false categoryExpanded = false
}) { }) {
Text(text = category.icon, fontSize = 20.sp) Text(text = category.icon, fontSize = 20.sp)
@@ -512,7 +524,7 @@ fun BillAdditionalDetailsSection(
var pmExpanded by remember { mutableStateOf(false) } var pmExpanded by remember { mutableStateOf(false) }
val selectedPm = val selectedPm =
paymentModes.find { it.remoteId.toInt() == viewModel.paymentModeRemoteId } paymentModes.find { it.id == viewModel.paymentModeId } ?: CategoryUtils.getPaymentModeById(context, viewModel.paymentModeId)
EditableExposedDropdownMenu( EditableExposedDropdownMenu(
value = selectedPm?.name ?: "", value = selectedPm?.name ?: "",
@@ -532,7 +544,7 @@ fun BillAdditionalDetailsSection(
}, },
content = { content = {
DropdownMenuItem(onClick = { DropdownMenuItem(onClick = {
viewModel.paymentModeRemoteId = 0 viewModel.paymentModeId = 0
pmExpanded = false pmExpanded = false
}) { }) {
Icon(Icons.Default.Close, tint = Color.Red, contentDescription = null) Icon(Icons.Default.Close, tint = Color.Red, contentDescription = null)
@@ -541,7 +553,7 @@ fun BillAdditionalDetailsSection(
} }
paymentModes.forEach { pm -> paymentModes.forEach { pm ->
DropdownMenuItem(onClick = { DropdownMenuItem(onClick = {
viewModel.paymentModeRemoteId = pm.remoteId.toInt() viewModel.paymentModeId = pm.id
pmExpanded = false pmExpanded = false
}) { }) {
Text(text = pm.icon, fontSize = 20.sp) Text(text = pm.icon, fontSize = 20.sp)
@@ -2,22 +2,20 @@ package net.helcel.cowspent.android.bill_edit
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.android.helper.DialogState import net.helcel.cowspent.android.helper.DialogState
import net.helcel.cowspent.android.helper.parseAmount import net.helcel.cowspent.android.helper.parseAmount
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCurrency
import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.util.SupportUtil import net.helcel.cowspent.util.SupportUtil
import net.helcel.cowspent.util.evalMath import net.helcel.cowspent.util.evalMath
import net.helcel.cowspent.model.DBCurrency
import androidx.compose.ui.graphics.vector.ImageVector
class EditBillViewModel : ViewModel() { class EditBillViewModel : ViewModel() {
var what by mutableStateOf("") var what by mutableStateOf("")
var amount by mutableStateOf("") var amount by mutableStateOf("")
@@ -25,8 +23,8 @@ class EditBillViewModel : ViewModel() {
var timestamp by mutableLongStateOf(0L) var timestamp by mutableLongStateOf(0L)
var payerId by mutableLongStateOf(0L) var payerId by mutableLongStateOf(0L)
var repeat by mutableStateOf(DBBill.NON_REPEATED) var repeat by mutableStateOf(DBBill.NON_REPEATED)
var paymentModeRemoteId by mutableIntStateOf(0) var paymentModeId by mutableLongStateOf(0L)
var categoryRemoteId by mutableIntStateOf(0) var categoryId by mutableLongStateOf(0L)
var isNewBill by mutableStateOf(false) var isNewBill by mutableStateOf(false)
var currencies by mutableStateOf<List<DBCurrency>>(emptyList()) var currencies by mutableStateOf<List<DBCurrency>>(emptyList())
@@ -166,8 +164,8 @@ class EditBillViewModel : ViewModel() {
timestamp = bill.timestamp timestamp = bill.timestamp
payerId = bill.payerId payerId = bill.payerId
repeat = bill.repeat ?: DBBill.NON_REPEATED repeat = bill.repeat ?: DBBill.NON_REPEATED
paymentModeRemoteId = bill.paymentModeRemoteId paymentModeId = bill.paymentModeId
categoryRemoteId = bill.categoryRemoteId categoryId = bill.categoryId
val rawComment = bill.comment ?: "" val rawComment = bill.comment ?: ""
@@ -13,7 +13,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.theme.ThemeUtils import net.helcel.cowspent.theme.ThemeUtils
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.ProjectType import net.helcel.cowspent.model.ProjectType
@@ -39,24 +38,20 @@ class LabelBillsActivity : AppCompatActivity() {
val members = db.getMembersOfProject(projectId, null) val members = db.getMembersOfProject(projectId, null)
val allBills = db.getBillsOfProject(projectId) val allBills = db.getBillsOfProject(projectId)
val billsToLabel = allBills.filter { it.categoryRemoteId == 0 && it.state != DBBill.STATE_DELETED } val billsToLabel = allBills.filter { it.categoryId == 0L && it.state != DBBill.STATE_DELETED }
val allCategorized = allBills.filter { it.categoryRemoteId != 0 && it.state != DBBill.STATE_DELETED } val allCategorized = allBills.filter { it.categoryId != 0L && it.state != DBBill.STATE_DELETED }
val syncedCategories = db.getCategories(projectId) db.ensureDefaultLabels(projectId, projectType)
val defaultCategories = CategoryUtils.getDefaultCategories(this@LabelBillsActivity, projectId)
val hardcoded = if (projectType == ProjectType.LOCAL) { // Reload from DB to get real IDs
defaultCategories val categories = db.getCategories(projectId)
} else {
listOfNotNull(defaultCategories.find { it.remoteId.toInt() == DBBill.CATEGORY_REIMBURSEMENT })
}
val categories = syncedCategories + hardcoded
Quadruple(members, billsToLabel, categories, allCategorized) Quadruple(members, billsToLabel, categories, allCategorized)
} }
viewModel.billsToLabel = billsToLabel viewModel.billsToLabel = billsToLabel
viewModel.categories = categories viewModel.categories = categories
viewModel.categoriesMap = categories.associateBy { it.remoteId } viewModel.categoriesMap = categories.associateBy { it.id }
viewModel.allCategorizedBills = allCategorized viewModel.allCategorizedBills = allCategorized
viewModel.updateSuggestions() viewModel.updateSuggestions()
@@ -84,7 +84,7 @@ fun LabelBillsScreen(
CategoryButton( CategoryButton(
icon = category.icon, icon = category.icon,
name = category.name ?: "", name = category.name ?: "",
onClick = { viewModel.labelCurrentBill(db, category.remoteId.toInt()) } onClick = { viewModel.labelCurrentBill(db, category.id) }
) )
} }
} }
@@ -126,7 +126,7 @@ fun LabelBillsScreen(
CategoryButton( CategoryButton(
icon = category.icon, icon = category.icon,
name = category.name ?: "", name = category.name ?: "",
onClick = { viewModel.labelCurrentBill(db, category.remoteId.toInt()) } onClick = { viewModel.labelCurrentBill(db, category.id) }
) )
} }
} }
@@ -245,16 +245,16 @@ fun CategoryButton(icon: String, name: String, onClick: () -> Unit) {
fun LabelBillsScreenPreview() { fun LabelBillsScreenPreview() {
val viewModel = LabelBillsViewModel().apply { val viewModel = LabelBillsViewModel().apply {
billsToLabel = listOf( billsToLabel = listOf(
DBBill(1L, 0, 1L, 1L, 120.5, System.currentTimeMillis() / 1000, "Groceries at Aldi", 0, null, null, 0, null, -1) DBBill(1L, 0, 1L, 1L, 120.5, System.currentTimeMillis() / 1000, "Groceries at Aldi", 0, null, null, 0L, null, -1L)
) )
val cats = listOf( val cats = listOf(
DBCategory(1, 1, 1, "Groceries", "🛒", ""), DBCategory(1L, 1L, 1L, "Groceries", "🛒", ""),
DBCategory(2, 2, 1, "Leisure", "🥳", ""), DBCategory(2L, 2L, 1L, "Leisure", "🥳", ""),
DBCategory(3, 3, 1, "Rent", "🏠", ""), DBCategory(3L, 3L, 1L, "Rent", "🏠", ""),
DBCategory(4, 4, 1, "Bills", "💸", "") DBCategory(4L, 4L, 1L, "Bills", "💸", "")
) )
categories = cats categories = cats
categoriesMap = cats.associateBy { it.remoteId } categoriesMap = cats.associateBy { it.id }
} }
val members = listOf( val members = listOf(
DBMember(1L, 0, 1L, "Alice", true, 1.0, 0, 255, 100, 100, null, null), DBMember(1L, 0, 1L, "Alice", true, 1.0, 0, 255, 100, 100, null, null),
@@ -43,18 +43,18 @@ class LabelBillsViewModel : ViewModel() {
otherName == name || (name.length > 3 && otherName.contains(name)) || (otherName.length > 3 && name.contains(otherName)) otherName == name || (name.length > 3 && otherName.contains(name)) || (otherName.length > 3 && name.contains(otherName))
} }
val counts = matches.groupBy { it.categoryRemoteId } val counts = matches.groupBy { it.categoryId }
.mapValues { it.value.size } .mapValues { it.value.size }
.toList() .toList()
.sortedByDescending { it.second } .sortedByDescending { it.second }
.take(2) .take(2)
suggestedCategories = counts.mapNotNull { (catId, _) -> suggestedCategories = counts.mapNotNull { (catId, _) ->
categoriesMap[catId.toLong()] categoriesMap[catId]
} }
} }
fun labelCurrentBill(db: CowspentSQLiteOpenHelper, categoryId: Int) { fun labelCurrentBill(db: CowspentSQLiteOpenHelper, categoryId: Long) {
currentBill?.let { bill -> currentBill?.let { bill ->
db.updateBillAndSync( db.updateBillAndSync(
bill = bill, bill = bill,
@@ -65,11 +65,11 @@ class LabelBillsViewModel : ViewModel() {
newOwersIds = bill.billOwersIds, newOwersIds = bill.billOwersIds,
newRepeat = bill.repeat, newRepeat = bill.repeat,
newPaymentMode = bill.paymentMode, newPaymentMode = bill.paymentMode,
newPaymentModeRemoteId = bill.paymentModeRemoteId, newPaymentModeId = bill.paymentModeId,
newCategoryId = categoryId, newCategoryId = categoryId,
newComment = bill.comment newComment = bill.comment
) )
bill.categoryRemoteId = categoryId bill.categoryId = categoryId
onBillProcessed?.invoke(bill.id) onBillProcessed?.invoke(bill.id)
moveToNext() moveToNext()
} }
@@ -87,11 +87,11 @@ class LabelBillsViewModel : ViewModel() {
val start = currentBillIndex val start = currentBillIndex
var next = (start + 1) % billsToLabel.size var next = (start + 1) % billsToLabel.size
while (next != start && billsToLabel[next].categoryRemoteId != 0) { while (next != start && billsToLabel[next].categoryId != 0L) {
next = (next + 1) % billsToLabel.size next = (next + 1) % billsToLabel.size
} }
currentBillIndex = if (billsToLabel[next].categoryRemoteId == 0) { currentBillIndex = if (billsToLabel[next].categoryId == 0L) {
next next
} else { } else {
billsToLabel.size billsToLabel.size
@@ -33,9 +33,11 @@ class ManageCurrenciesActivity : AppCompatActivity() {
if (message.isEmpty()) { if (message.isEmpty()) {
showToast(this@ManageCurrenciesActivity,getString(R.string.currency_saved_success), Toast.LENGTH_LONG) showToast(this@ManageCurrenciesActivity,getString(R.string.currency_saved_success), Toast.LENGTH_LONG)
} else { } else {
viewModel.showDialog(title=getString(R.string.error_edit_remote_project_helper, message), viewModel.showDialog(
message=getString(R.string.action_currencies), title = getString(R.string.dialog_sync_error_title),
positiveText = getString(android.R.string.ok)) message = getString(R.string.error_edit_remote_project_helper, message),
positiveText = getString(android.R.string.ok)
)
} }
} }
override fun onScheduled() {} override fun onScheduled() {}
@@ -28,6 +28,7 @@ import androidx.core.graphics.toColorInt
import net.helcel.cowspent.R import net.helcel.cowspent.R
import net.helcel.cowspent.android.helper.ColorPicker import net.helcel.cowspent.android.helper.ColorPicker
import net.helcel.cowspent.android.helper.StatefulAlertDialog import net.helcel.cowspent.android.helper.StatefulAlertDialog
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCategory import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBPaymentMode import net.helcel.cowspent.model.DBPaymentMode
@@ -59,10 +60,10 @@ fun LabelManagementScreenContent(
dialogState: net.helcel.cowspent.android.helper.DialogState?, dialogState: net.helcel.cowspent.android.helper.DialogState?,
onBack: () -> Unit, onBack: () -> Unit,
onAddCategory: (String, String, String) -> Unit, onAddCategory: (String, String, String) -> Unit,
onUpdateCategory: (Long, String, String, String) -> Unit, onUpdateCategory: (DBCategory, String, String, String) -> Unit,
onDeleteCategory: (Long) -> Unit, onDeleteCategory: (Long) -> Unit,
onAddPaymentMode: (String, String, String) -> Unit, onAddPaymentMode: (String, String, String) -> Unit,
onUpdatePaymentMode: (Long, String, String, String) -> Unit, onUpdatePaymentMode: (DBPaymentMode, String, String, String) -> Unit,
onDeletePaymentMode: (Long) -> Unit, onDeletePaymentMode: (Long) -> Unit,
onDismissDialog: () -> Unit, onDismissDialog: () -> Unit,
onShowDialog: (net.helcel.cowspent.android.helper.DialogState) -> Unit, onShowDialog: (net.helcel.cowspent.android.helper.DialogState) -> Unit,
@@ -123,7 +124,7 @@ fun LabelManagementScreenContent(
if (selectedTab == 0) { if (selectedTab == 0) {
CategoryList( CategoryList(
categories = categories, categories = categories.filter { it.remoteId != DBBill.CATEGORY_REIMBURSEMENT },
onEdit = { onEdit = {
editingCategory = it editingCategory = it
showEditDialog = true showEditDialog = true
@@ -162,7 +163,7 @@ fun LabelManagementScreenContent(
if (showEditDialog) { if (showEditDialog) {
if (selectedTab == 0) { if (selectedTab == 0) {
EditLabelDialog( EditLabelDialog(
title = if (editingCategory == null) stringResource(R.string.action_add_project) else stringResource(R.string.action_edit), title = if (editingCategory == null) stringResource(R.string.title_add_category) else stringResource(R.string.action_edit),
initialName = editingCategory?.name ?: "", initialName = editingCategory?.name ?: "",
initialIcon = editingCategory?.icon ?: "", initialIcon = editingCategory?.icon ?: "",
initialColor = editingCategory?.color ?: "#FF0000", initialColor = editingCategory?.color ?: "#FF0000",
@@ -171,14 +172,14 @@ fun LabelManagementScreenContent(
if (editingCategory == null) { if (editingCategory == null) {
onAddCategory(name, icon, color) onAddCategory(name, icon, color)
} else { } else {
onUpdateCategory(editingCategory!!.id, name, icon, color) onUpdateCategory(editingCategory!!, name, icon, color)
} }
showEditDialog = false showEditDialog = false
} }
) )
} else { } else {
EditLabelDialog( EditLabelDialog(
title = if (editingPaymentMode == null) stringResource(R.string.action_add_project) else stringResource(R.string.action_edit), title = if (editingPaymentMode == null) stringResource(R.string.title_add_payment_mode) else stringResource(R.string.action_edit),
initialName = editingPaymentMode?.name ?: "", initialName = editingPaymentMode?.name ?: "",
initialIcon = editingPaymentMode?.icon ?: "", initialIcon = editingPaymentMode?.icon ?: "",
initialColor = editingPaymentMode?.color ?: "#00FF00", initialColor = editingPaymentMode?.color ?: "#00FF00",
@@ -187,7 +188,7 @@ fun LabelManagementScreenContent(
if (editingPaymentMode == null) { if (editingPaymentMode == null) {
onAddPaymentMode(name, icon, color) onAddPaymentMode(name, icon, color)
} else { } else {
onUpdatePaymentMode(editingPaymentMode!!.id, name, icon, color) onUpdatePaymentMode(editingPaymentMode!!, name, icon, color)
} }
showEditDialog = false showEditDialog = false
} }
@@ -353,8 +354,8 @@ fun LabelManagementCategoriesPreview() {
MaterialTheme { MaterialTheme {
LabelManagementScreenContent( LabelManagementScreenContent(
categories = listOf( categories = listOf(
DBCategory(1, 1, 1, "Groceries", "🛒", "#FF0000"), DBCategory(1L, 1L, 1L, "Groceries", "🛒", "#FF0000"),
DBCategory(2, 2, 1, "Rent", "🏠", "#00FF00") DBCategory(2L, 2L, 1L, "Rent", "🏠", "#00FF00")
), ),
paymentModes = emptyList(), paymentModes = emptyList(),
dialogState = null, dialogState = null,
@@ -379,8 +380,8 @@ fun LabelManagementPaymentModesPreview() {
LabelManagementScreenContent( LabelManagementScreenContent(
categories = emptyList(), categories = emptyList(),
paymentModes = listOf( paymentModes = listOf(
DBPaymentMode(1, 1, 1, "Cash", "💵", "#0000FF"), DBPaymentMode(1L, 1L, 1L, "Cash", "💵", "#0000FF"),
DBPaymentMode(2, 2, 1, "Credit Card", "💳", "#FFFF00") DBPaymentMode(2L, 2L, 1L, "Credit Card", "💳", "#FFFF00")
), ),
dialogState = null, dialogState = null,
onBack = {}, onBack = {},
@@ -13,8 +13,8 @@ import kotlinx.coroutines.withContext
import net.helcel.cowspent.android.helper.DialogState import net.helcel.cowspent.android.helper.DialogState
import net.helcel.cowspent.model.DBCategory import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBPaymentMode import net.helcel.cowspent.model.DBPaymentMode
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.util.CategoryUtils
class LabelManagementViewModel(application: Application) : AndroidViewModel(application) { class LabelManagementViewModel(application: Application) : AndroidViewModel(application) {
private val db = CowspentSQLiteOpenHelper.getInstance(application) private val db = CowspentSQLiteOpenHelper.getInstance(application)
@@ -28,44 +28,30 @@ class LabelManagementViewModel(application: Application) : AndroidViewModel(appl
fun loadLabels(projId: Long) { fun loadLabels(projId: Long) {
projectId = projId projectId = projId
viewModelScope.launch { viewModelScope.launch {
val cats = withContext(Dispatchers.IO) { db.getCategories(projId) } val project = withContext(Dispatchers.IO) { db.getProject(projId) }
val pms = withContext(Dispatchers.IO) { db.getPaymentModes(projId) } if (project != null) {
if (cats.isEmpty()) {
val defaults = CategoryUtils.getDefaultCategories(getApplication(), projId)
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
defaults.forEach { db.addCategory(it) } db.ensureDefaultLabels(projId, project.type)
} }
categories = withContext(Dispatchers.IO) { db.getCategories(projId) }
} else {
categories = cats
}
if (pms.isEmpty()) {
val defaults = CategoryUtils.getDefaultPaymentModes(getApplication(), projId)
withContext(Dispatchers.IO) {
defaults.forEach { db.addPaymentMode(it) }
}
paymentModes = withContext(Dispatchers.IO) { db.getPaymentModes(projId) }
} else {
paymentModes = pms
} }
categories = withContext(Dispatchers.IO) { db.getCategories(projId) }
paymentModes = withContext(Dispatchers.IO) { db.getPaymentModes(projId) }
} }
} }
fun addCategory(name: String, icon: String, color: String) { fun addCategory(name: String, icon: String, color: String) {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
db.addCategory(DBCategory(0, 0, projectId, name, icon, color)) db.addCategoryAndSync(DBCategory(0, 0, projectId, name, icon, color, DBBill.STATE_ADDED))
} }
loadLabels(projectId) loadLabels(projectId)
} }
} }
fun updateCategory(id: Long, name: String, icon: String, color: String) { fun updateCategory(cat: DBCategory, name: String, icon: String, color: String) {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
db.updateCategory(id, name, icon, color) db.updateCategoryAndSync(cat, name, icon, color)
} }
loadLabels(projectId) loadLabels(projectId)
} }
@@ -74,7 +60,7 @@ class LabelManagementViewModel(application: Application) : AndroidViewModel(appl
fun deleteCategory(id: Long) { fun deleteCategory(id: Long) {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
db.deleteCategory(id) db.deleteCategoryAndSync(id)
} }
loadLabels(projectId) loadLabels(projectId)
} }
@@ -83,16 +69,16 @@ class LabelManagementViewModel(application: Application) : AndroidViewModel(appl
fun addPaymentMode(name: String, icon: String, color: String) { fun addPaymentMode(name: String, icon: String, color: String) {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
db.addPaymentMode(DBPaymentMode(0, 0, projectId, name, icon, color)) db.addPaymentModeAndSync(DBPaymentMode(0, 0, projectId, name, icon, color, DBBill.STATE_ADDED))
} }
loadLabels(projectId) loadLabels(projectId)
} }
} }
fun updatePaymentMode(id: Long, name: String, icon: String, color: String) { fun updatePaymentMode(pm: DBPaymentMode, name: String, icon: String, color: String) {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
db.updatePaymentMode(id, name, icon, color) db.updatePaymentModeAndSync(pm, name, icon, color)
} }
loadLabels(projectId) loadLabels(projectId)
} }
@@ -101,7 +87,7 @@ class LabelManagementViewModel(application: Application) : AndroidViewModel(appl
fun deletePaymentMode(id: Long) { fun deletePaymentMode(id: Long) {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
db.deletePaymentMode(id) db.deletePaymentModeAndSync(id)
} }
loadLabels(projectId) loadLabels(projectId)
} }
@@ -103,6 +103,10 @@ fun BillsListScreen(
val context = LocalContext.current val context = LocalContext.current
val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) } val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) }
val showArchived = sharedPreferences.getBoolean(stringResource(R.string.pref_key_show_archived), false) val showArchived = sharedPreferences.getBoolean(stringResource(R.string.pref_key_show_archived), false)
val keyBetaFeatures = stringResource(R.string.pref_key_beta_features)
var showBetaFeatures by remember(keyBetaFeatures) {
mutableStateOf(sharedPreferences.getBoolean(keyBetaFeatures, false))
}
StatefulAlertDialog( StatefulAlertDialog(
state = viewModel.dialogState, state = viewModel.dialogState,
@@ -158,7 +162,8 @@ fun BillsListScreen(
isArchived = proj?.isArchived == true, isArchived = proj?.isArchived == true,
projectType = proj?.type ?: ProjectType.LOCAL, projectType = proj?.type ?: ProjectType.LOCAL,
accessLevel = proj?.myAccessLevel ?: DBProject.ACCESS_LEVEL_ADMIN, accessLevel = proj?.myAccessLevel ?: DBProject.ACCESS_LEVEL_ADMIN,
isShareable = proj?.isShareable() ?: true isShareable = proj?.isShareable() ?: true,
showBetaFeatures = showBetaFeatures
) )
} }
} }
@@ -327,7 +332,7 @@ fun BillsListScreen(
}, },
actions = { actions = {
if (!isSearchExpanded) { if (!isSearchExpanded) {
if (viewModel.hasUnlabeledBills) { if (showBetaFeatures && viewModel.hasUnlabeledBills) {
IconButton(onClick = onLabelBillsClick) { IconButton(onClick = onLabelBillsClick) {
Icon( Icon(
Icons.Default.Category, Icons.Default.Category,
@@ -5,6 +5,7 @@ import android.content.Intent
import net.helcel.cowspent.R import net.helcel.cowspent.R
import net.helcel.cowspent.model.* import net.helcel.cowspent.model.*
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.IRefreshBillsListCallback import net.helcel.cowspent.util.IRefreshBillsListCallback
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@@ -98,6 +99,10 @@ object BillsListUtils {
context: Context context: Context
) { ) {
val timestamp = System.currentTimeMillis() / 1000 val timestamp = System.currentTimeMillis() / 1000
val proj = db.getProject(projectId)
val categories = db.getCategories(projectId)
val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(categories, projectId, proj?.remoteId)
for (t in transactions) { for (t in transactions) {
val owerId = t.owerMemberId val owerId = t.owerMemberId
val receiverId = t.receiverMemberId val receiverId = t.receiverMemberId
@@ -106,7 +111,7 @@ object BillsListUtils {
0, 0, projectId, owerId, amount, 0, 0, projectId, owerId, amount,
timestamp, context.getString(R.string.settle_bill_what), timestamp, context.getString(R.string.settle_bill_what),
DBBill.STATE_ADDED, DBBill.NON_REPEATED, DBBill.STATE_ADDED, DBBill.NON_REPEATED,
DBBill.PAYMODE_NONE, DBBill.CATEGORY_NONE, DBBill.PAYMODE_NONE, reimbursementCategoryId,
"", DBBill.PAYMODE_ID_NONE "", DBBill.PAYMODE_ID_NONE
) )
bill.billOwers += DBBillOwer(0, 0, receiverId) bill.billOwers += DBBillOwer(0, 0, receiverId)
@@ -21,9 +21,7 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircleOutline
import androidx.compose.material.icons.filled.Sync import androidx.compose.material.icons.filled.Sync
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.edit import androidx.core.content.edit
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -50,6 +48,7 @@ import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.persistence.CowspentServerSyncHelper import net.helcel.cowspent.persistence.CowspentServerSyncHelper
import net.helcel.cowspent.theme.ThemeUtils import net.helcel.cowspent.theme.ThemeUtils
import net.helcel.cowspent.util.BillFormatter import net.helcel.cowspent.util.BillFormatter
import net.helcel.cowspent.util.CategoryUtils
import net.helcel.cowspent.util.CospendClientUtil import net.helcel.cowspent.util.CospendClientUtil
import net.helcel.cowspent.util.ExportUtil import net.helcel.cowspent.util.ExportUtil
import net.helcel.cowspent.util.ICallback import net.helcel.cowspent.util.ICallback
@@ -115,18 +114,8 @@ class BillsListViewActivity :
Log.d(TAG, "CREATED project id: $pid") Log.d(TAG, "CREATED project id: $pid")
lifecycleScope.launch { lifecycleScope.launch {
val addedProj = withContext(Dispatchers.IO) { db.getProject(pid) } val addedProj = withContext(Dispatchers.IO) { db.getProject(pid) }
val message: String val message = getString(R.string.msg_project_added, addedProj?.name?.ifEmpty { addedProj.remoteId } ?: pid.toString())
val title: String showToast(this@BillsListViewActivity, message)
if (created) {
Log.e(TAG, "CREATED !!!")
title = getString(R.string.msg_project_added)
message = getString(R.string.msg_project_added, addedProj?.remoteId)
} else {
Log.e(TAG, "ADDED !!!")
title = getString(R.string.msg_project_added)
message = getString(R.string.msg_project_added, addedProj?.remoteId)
}
showDialog(message, title, Icons.Default.AddCircleOutline)
} }
} }
} }
@@ -142,7 +131,7 @@ class BillsListViewActivity :
} }
if (!db.cowspentServerSyncHelper.isSyncPossible) { if (!db.cowspentServerSyncHelper.isSyncPossible) {
if (CowspentServerSyncHelper.isNextcloudAccountConfigured(applicationContext)) { if (CowspentServerSyncHelper.isNextcloudAccountConfigured(applicationContext)) {
Toast.makeText(applicationContext, getString(R.string.error_sync, getString(CospendClientUtil.LoginStatus.NO_NETWORK.str)), Toast.LENGTH_LONG).show() showToast(this@BillsListViewActivity, getString(R.string.error_sync, getString(CospendClientUtil.LoginStatus.NO_NETWORK.str)), Toast.LENGTH_LONG)
} }
} }
} }
@@ -565,11 +554,13 @@ class BillsListViewActivity :
val members = db.getMembersOfProject(proj.id, null) val members = db.getMembersOfProject(proj.id, null)
val bills = db.getBillsOfProject(proj.id) val bills = db.getBillsOfProject(proj.id)
val categories = db.getCategories(proj.id)
val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(categories, proj.id, proj.remoteId)
val balances = HashMap<Long, Double>() val balances = HashMap<Long, Double>()
SupportUtil.getStats( SupportUtil.getStats(
members, bills, members, bills,
mutableMapOf(), balances, mutableMapOf(), mutableMapOf(), mutableMapOf(), balances, mutableMapOf(), mutableMapOf(),
-1000, -1000, null, null -1000L, -1000L, reimbursementCategoryId, null, null
) )
Triple(proj, members, balances) Triple(proj, members, balances)
} }
@@ -636,13 +627,13 @@ class BillsListViewActivity :
mid == null || mid == it.payerId || it.billOwersIds.contains(mid) mid == null || mid == it.payerId || it.billOwersIds.contains(mid)
} }
viewModel.hasUnlabeledBills = bills.any { it.categoryRemoteId == 0 && it.state != DBBill.STATE_DELETED } viewModel.hasUnlabeledBills = bills.any { it.categoryId == 0L && it.state != DBBill.STATE_DELETED }
val projectMembers = db.getMembersOfProject(projId, null) val projectMembers = db.getMembersOfProject(projId, null)
val memberMap = projectMembers.associateBy { it.id } val memberMap = projectMembers.associateBy { it.id }
val projectPaymentModes = db.getPaymentModes(projId).associateBy { it.remoteId } val projectPaymentModes = db.getPaymentModes(projId).associateBy { it.id }
val projectCategories = db.getCategories(projId).associateBy { it.remoteId } val projectCategories = db.getCategories(projId).associateBy { it.id }
BillFormatter.formatBills( BillFormatter.formatBills(
bills, bills,
@@ -708,15 +699,6 @@ class BillsListViewActivity :
} }
} }
private fun showDialog(msg: String, title: String, icon: ImageVector) {
viewModel.showDialog(
title = title,
message = msg,
positiveText = getString(android.R.string.ok),
icon = icon
)
}
private fun updateUsernameInDrawer() { private fun updateUsernameInDrawer() {
if (!CowspentServerSyncHelper.isNextcloudAccountConfigured(this)) { if (!CowspentServerSyncHelper.isNextcloudAccountConfigured(this)) {
val text = getString(R.string.drawer_no_account) val text = getString(R.string.drawer_no_account)
@@ -118,8 +118,8 @@ object ProjectImportHelper {
val payerWeight = if (columns.containsKey("payer_weight")) line[columns["payer_weight"]!!].toDouble() else 1.0 val payerWeight = if (columns.containsKey("payer_weight")) line[columns["payer_weight"]!!].toDouble() else 1.0
val owersStr = if (columns.containsKey("owers")) line[columns["owers"]!!] else "" val owersStr = if (columns.containsKey("owers")) line[columns["owers"]!!] else ""
val payerActive = columns.containsKey("payer_active") && line[columns["payer_active"]!!] == "1" val payerActive = columns.containsKey("payer_active") && line[columns["payer_active"]!!] == "1"
val catId = if (columns.containsKey("categoryid") && line[columns["categoryid"]!!].isNotEmpty()) line[columns["categoryid"]!!].toInt() else 0 val catId = if (columns.containsKey("categoryid") && line[columns["categoryid"]!!].isNotEmpty()) line[columns["categoryid"]!!].toLong() else 0L
val pmId = if (columns.containsKey("paymentmodeid") && line[columns["paymentmodeid"]!!].isNotEmpty()) line[columns["paymentmodeid"]!!].toInt() else 0 val pmId = if (columns.containsKey("paymentmodeid") && line[columns["paymentmodeid"]!!].isNotEmpty()) line[columns["paymentmodeid"]!!].toLong() else 0L
val pm = if (columns.containsKey("paymentmode")) line[columns["paymentmode"]!!] else null val pm = if (columns.containsKey("paymentmode")) line[columns["paymentmode"]!!] else null
membersActive[payerName] = payerActive membersActive[payerName] = payerActive
@@ -149,9 +149,17 @@ object ProjectImportHelper {
val memberNameToId = mutableMapOf<String, Long>() val memberNameToId = mutableMapOf<String, Long>()
val pid = db.addProject(DBProject(0, projectRemoteId, "", projectRemoteId, null, null, null, ProjectType.LOCAL, 0L, mainCurrencyName, false, DBProject.ACCESS_LEVEL_UNKNOWN, null)) val pid = db.addProject(DBProject(0, projectRemoteId, "", projectRemoteId, null, null, null, ProjectType.LOCAL, 0L, mainCurrencyName, false, DBProject.ACCESS_LEVEL_UNKNOWN, null))
paymentModes.forEach { db.addPaymentMode(DBPaymentMode(0, it.remoteId, pid, it.name, it.icon, it.color)) } val pmRemoteToLocal = mutableMapOf<Long, Long>()
categories.forEach { db.addCategory(DBCategory(0, it.remoteId, pid, it.name, it.icon, it.color)) } paymentModes.forEach {
val newId = db.addPaymentMode(DBPaymentMode(0, it.remoteId, pid, it.name, it.icon, it.color))
pmRemoteToLocal[it.remoteId] = newId
}
val catRemoteToLocal = mutableMapOf<Long, Long>()
categories.forEach {
val newId = db.addCategory(DBCategory(0, it.remoteId, pid, it.name, it.icon, it.color))
catRemoteToLocal[it.remoteId] = newId
}
currencies.forEach { db.addCurrency(DBCurrency(0, 0, pid, it.name, it.exchangeRate, DBBill.STATE_OK)) } currencies.forEach { db.addCurrency(DBCurrency(0, 0, pid, it.name, it.exchangeRate, DBBill.STATE_OK)) }
membersWeight.keys.forEach { mName -> membersWeight.keys.forEach { mName ->
@@ -160,7 +168,9 @@ object ProjectImportHelper {
bills.forEach { b -> bills.forEach { b ->
val payerId = memberNameToId[billRemoteIdToPayerName[b.remoteId]] ?: 0L val payerId = memberNameToId[billRemoteIdToPayerName[b.remoteId]] ?: 0L
val billId = db.addBill(DBBill(0, 0, pid, payerId, b.amount, b.timestamp, b.what, DBBill.STATE_OK, b.repeat, b.paymentMode, b.categoryRemoteId, b.comment, b.paymentModeRemoteId)) val localCatId = catRemoteToLocal[b.categoryId] ?: 0L
val localPmId = pmRemoteToLocal[b.paymentModeId] ?: 0L
val billId = db.addBill(DBBill(0, 0, pid, payerId, b.amount, b.timestamp, b.what, DBBill.STATE_OK, b.repeat, b.paymentMode, localCatId, b.comment, localPmId))
billRemoteIdToOwerStr[b.remoteId]?.split(", ")?.filter { it.isNotEmpty() }?.forEach { ower -> billRemoteIdToOwerStr[b.remoteId]?.split(", ")?.filter { it.isNotEmpty() }?.forEach { ower ->
memberNameToId[ower.trim()]?.let { owerId -> db.addBillower(billId, owerId) } memberNameToId[ower.trim()]?.let { owerId -> db.addBillower(billId, owerId) }
} }
@@ -35,7 +35,8 @@ fun ProjectOptionsDialogContent(
isArchived: Boolean = false, isArchived: Boolean = false,
projectType: ProjectType = ProjectType.LOCAL, projectType: ProjectType = ProjectType.LOCAL,
accessLevel: Int = DBProject.ACCESS_LEVEL_ADMIN, accessLevel: Int = DBProject.ACCESS_LEVEL_ADMIN,
isShareable: Boolean = true isShareable: Boolean = true,
showBetaFeatures: Boolean = false
) { ) {
Surface( Surface(
shape = MaterialTheme.shapes.large, shape = MaterialTheme.shapes.large,
@@ -79,7 +80,7 @@ fun ProjectOptionsDialogContent(
// Row 2: Manage Member, Manage Labels, Manage Currencies // Row 2: Manage Member, Manage Labels, Manage Currencies
if (!isArchived && isMaintainer) { if (!isArchived && isMaintainer) {
row2.add(ProjectOption(stringResource(R.string.action_members), Icons.Default.Group, onManageMembers)) row2.add(ProjectOption(stringResource(R.string.action_members), Icons.Default.Group, onManageMembers))
if (projectType == ProjectType.LOCAL || projectType == ProjectType.COSPEND) { if (showBetaFeatures && (projectType == ProjectType.LOCAL || projectType == ProjectType.COSPEND)) {
row2.add(ProjectOption(stringResource(R.string.action_labels), Icons.AutoMirrored.Filled.Label, onManageLabels)) row2.add(ProjectOption(stringResource(R.string.action_labels), Icons.AutoMirrored.Filled.Label, onManageLabels))
} }
row2.add(ProjectOption(stringResource(R.string.action_currencies), Icons.Default.MonetizationOn, onManageCurrencies)) row2.add(ProjectOption(stringResource(R.string.action_currencies), Icons.Default.MonetizationOn, onManageCurrencies))
@@ -181,7 +182,8 @@ fun ProjectOptionsDialogPreview() {
isArchived = false, isArchived = false,
projectType = ProjectType.COSPEND, projectType = ProjectType.COSPEND,
accessLevel = DBProject.ACCESS_LEVEL_ADMIN, accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
isShareable = true isShareable = true,
showBetaFeatures = true
) )
} }
} }
@@ -205,7 +207,8 @@ fun ProjectOptionsDialogPreview2() {
isArchived = true, isArchived = true,
projectType = ProjectType.COSPEND, projectType = ProjectType.COSPEND,
accessLevel = DBProject.ACCESS_LEVEL_ADMIN, accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
isShareable = true isShareable = true,
showBetaFeatures = true
) )
} }
} }
@@ -228,7 +231,8 @@ fun ProjectOptionsDialogPreview3() {
isArchived = false, isArchived = false,
projectType = ProjectType.LOCAL, projectType = ProjectType.LOCAL,
accessLevel = DBProject.ACCESS_LEVEL_ADMIN, accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
isShareable = true isShareable = true,
showBetaFeatures = true
) )
} }
} }
@@ -320,7 +320,6 @@ class NewProjectActivity : AppCompatActivity() {
pid pid
) )
} }
showToast(getString(R.string.project_added_success), Toast.LENGTH_LONG)
return pid return pid
} }
@@ -79,6 +79,7 @@ fun SettingsScreen(
val keyColor = stringResource(R.string.pref_key_color) val keyColor = stringResource(R.string.pref_key_color)
val keyOfflineMode = stringResource(R.string.pref_key_offline_mode) val keyOfflineMode = stringResource(R.string.pref_key_offline_mode)
val keyShowArchived = stringResource(R.string.pref_key_show_archived) val keyShowArchived = stringResource(R.string.pref_key_show_archived)
val keyBetaFeatures = stringResource(R.string.pref_key_beta_features)
// States for preferences // States for preferences
var nightMode by remember(keyNightMode) { var nightMode by remember(keyNightMode) {
@@ -113,6 +114,9 @@ fun SettingsScreen(
var showArchived by remember(keyShowArchived) { var showArchived by remember(keyShowArchived) {
mutableStateOf(sharedPreferences.getBoolean(keyShowArchived, false)) mutableStateOf(sharedPreferences.getBoolean(keyShowArchived, false))
} }
var betaFeatures by remember(keyBetaFeatures) {
mutableStateOf(sharedPreferences.getBoolean(keyBetaFeatures, false))
}
Scaffold( Scaffold(
topBar = { topBar = {
@@ -242,6 +246,19 @@ fun SettingsScreen(
// Other // Other
SettingsCategory(stringResource(R.string.settings_other)) SettingsCategory(stringResource(R.string.settings_other))
SettingsSwitchPreference(
title = stringResource(R.string.settings_beta_features),
summary = stringResource(R.string.settings_beta_features_summary),
icon = Icons.Default.Info,
checked = betaFeatures,
onCheckedChange = {
betaFeatures = it
sharedPreferences.edit {
putBoolean(keyBetaFeatures, it)
}
}
)
SettingsPreference( SettingsPreference(
title = stringResource(R.string.title_about), title = stringResource(R.string.title_about),
icon = Icons.Default.Info, icon = Icons.Default.Info,
@@ -71,6 +71,7 @@ import net.helcel.cowspent.android.helper.formatShortValue
import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCategory import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBMember import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.util.CategoryUtils
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -87,6 +88,8 @@ private object SankeyDimens {
@Composable @Composable
fun ProjectSankeyDiagram( fun ProjectSankeyDiagram(
projectName: String, projectName: String,
projectId: Long,
projectRemoteId: String?,
allMembers: List<DBMember>, allMembers: List<DBMember>,
allBills: List<DBBill>, allBills: List<DBBill>,
customCategories: List<DBCategory>, customCategories: List<DBCategory>,
@@ -96,12 +99,15 @@ fun ProjectSankeyDiagram(
var selectedMemberId by remember { mutableLongStateOf(-1L) } var selectedMemberId by remember { mutableLongStateOf(-1L) }
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
val activeBills = remember(allBills) { val reimbursementCategoryId = remember(customCategories, projectId, projectRemoteId) {
allBills.filter { it.state != DBBill.STATE_DELETED && it.categoryRemoteId != DBBill.CATEGORY_REIMBURSEMENT } CategoryUtils.getReimbursementCategoryId(customCategories, projectId, projectRemoteId)
}
val activeBills = remember(allBills, reimbursementCategoryId) {
allBills.filter { it.state != DBBill.STATE_DELETED && it.categoryId != reimbursementCategoryId }
} }
val membersMap = remember(allMembers) { allMembers.associateBy { it.id } } val membersMap = remember(allMembers) { allMembers.associateBy { it.id } }
val categoriesMap = remember(customCategories) { customCategories.associateBy { it.remoteId.toInt() } } val categoriesMap = remember(customCategories) { customCategories.associateBy { it.id } }
if (activeBills.isEmpty()) { if (activeBills.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -112,13 +118,13 @@ fun ProjectSankeyDiagram(
val spendings = remember(activeBills, selectedMemberId, membersMap) { val spendings = remember(activeBills, selectedMemberId, membersMap) {
val spentMap = mutableMapOf<Long, Double>().apply { membersMap.keys.forEach { put(it, 0.0) } } val spentMap = mutableMapOf<Long, Double>().apply { membersMap.keys.forEach { put(it, 0.0) } }
val catMap = mutableMapOf<Int, Double>() val catMap = mutableMapOf<Long, Double>()
activeBills.forEach { bill -> activeBills.forEach { bill ->
val totalWeight = bill.billOwers.sumOf { membersMap[it.memberId]?.weight ?: 1.0 } val totalWeight = bill.billOwers.sumOf { membersMap[it.memberId]?.weight ?: 1.0 }
if (totalWeight > 0) { if (totalWeight > 0) {
if (selectedMemberId == -1L) { if (selectedMemberId == -1L) {
catMap[bill.categoryRemoteId] = (catMap[bill.categoryRemoteId] ?: 0.0) + bill.amount catMap[bill.categoryId] = (catMap[bill.categoryId] ?: 0.0) + bill.amount
bill.billOwers.forEach { bo -> bill.billOwers.forEach { bo ->
val weight = membersMap[bo.memberId]?.weight ?: 1.0 val weight = membersMap[bo.memberId]?.weight ?: 1.0
spentMap[bo.memberId] = (spentMap[bo.memberId] ?: 0.0) + (bill.amount / totalWeight) * weight spentMap[bo.memberId] = (spentMap[bo.memberId] ?: 0.0) + (bill.amount / totalWeight) * weight
@@ -126,7 +132,7 @@ fun ProjectSankeyDiagram(
} else { } else {
bill.billOwers.find { it.memberId == selectedMemberId }?.let { bo -> bill.billOwers.find { it.memberId == selectedMemberId }?.let { bo ->
val weight = membersMap[bo.memberId]?.weight ?: 1.0 val weight = membersMap[bo.memberId]?.weight ?: 1.0
catMap[bill.categoryRemoteId] = (catMap[bill.categoryRemoteId] ?: 0.0) + (bill.amount / totalWeight) * weight catMap[bill.categoryId] = (catMap[bill.categoryId] ?: 0.0) + (bill.amount / totalWeight) * weight
spentMap[selectedMemberId] = (spentMap[selectedMemberId] ?: 0.0) + (bill.amount / totalWeight) * weight spentMap[selectedMemberId] = (spentMap[selectedMemberId] ?: 0.0) + (bill.amount / totalWeight) * weight
} }
} }
@@ -215,9 +221,9 @@ private fun SankeyContent(
selectedMemberId: Long, selectedMemberId: Long,
totalAmount: Double, totalAmount: Double,
displayMemberSpendings: List<Pair<Long, Double>>, displayMemberSpendings: List<Pair<Long, Double>>,
displayCategorySpendings: List<Pair<Int, Double>>, displayCategorySpendings: List<Pair<Long, Double>>,
membersMap: Map<Long, DBMember>, membersMap: Map<Long, DBMember>,
categoriesMap: Map<Int, DBCategory> categoriesMap: Map<Long, DBCategory>
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -399,4 +405,4 @@ private fun DrawScope.drawSankeyFlow(startX: Float, startY: Float, startWidth: F
@Preview(showBackground = true, widthDp = 360, heightDp = 640) @Preview(showBackground = true, widthDp = 360, heightDp = 640)
@Composable @Composable
fun ProjectSankeyDiagramPreview() = MaterialTheme { ProjectSankeyDiagram("Test Project", StatisticsMockData.members, StatisticsMockData.bills, emptyList()) {} } fun ProjectSankeyDiagramPreview() = MaterialTheme { ProjectSankeyDiagram("Test Project", 1L, "1", StatisticsMockData.members, StatisticsMockData.bills, emptyList()) {} }
@@ -84,6 +84,21 @@ fun ProjectStatisticsScreen(
val statsData by produceState<StatisticsData?>(null, proj.id) { val statsData by produceState<StatisticsData?>(null, proj.id) {
value = withContext(Dispatchers.IO) { value = withContext(Dispatchers.IO) {
val syncedCategories = db.getCategories(proj.id)
val defaultCategories = CategoryUtils.getDefaultCategories(context, proj.id)
val toEnsure = if (proj.type == ProjectType.LOCAL) {
defaultCategories
} else {
listOfNotNull(defaultCategories.find { it.remoteId == DBBill.CATEGORY_REIMBURSEMENT })
}
toEnsure.forEach { def ->
if (syncedCategories.none { it.remoteId == def.remoteId }) {
db.addCategory(def)
}
}
val members = db.getMembersOfProject(proj.id, null) val members = db.getMembersOfProject(proj.id, null)
val bills = db.getBillsOfProject(proj.id) val bills = db.getBillsOfProject(proj.id)
val categories = db.getCategories(proj.id) val categories = db.getCategories(proj.id)
@@ -94,19 +109,12 @@ fun ProjectStatisticsScreen(
if (statsData != null) { if (statsData != null) {
val data = statsData!! val data = statsData!!
val defaultCategories = remember(proj.id) { CategoryUtils.getDefaultCategories(context, proj.id) } val categories = data.categories
val categories = remember(proj.type, data.categories, defaultCategories) {
val hardcoded = if (proj.type == ProjectType.LOCAL) {
defaultCategories
} else {
listOfNotNull(defaultCategories.find { it.remoteId.toInt() == DBBill.CATEGORY_REIMBURSEMENT })
}
(data.categories + hardcoded).distinctBy { it.remoteId }
}
val categoryNoneLabel = stringResource(R.string.category_none) val categoryNoneLabel = stringResource(R.string.category_none)
val sankeyCategories = remember(proj.id, data.categories, defaultCategories, categoryNoneLabel) { val sankeyCategories = remember(proj.id, data.categories, categoryNoneLabel) {
val noneCategory = DBCategory(0, 0, proj.id, categoryNoneLabel, "", "#9E9E9E") val noneCategory = DBCategory(0, 0, proj.id, categoryNoneLabel, "", "#9E9E9E")
(data.categories + defaultCategories + noneCategory).distinctBy { it.remoteId } (data.categories + noneCategory).distinctBy { if (it.id == 0L) "none" else it.id.toString() }
} }
when (selectedTab) { when (selectedTab) {
@@ -131,6 +139,8 @@ fun ProjectStatisticsScreen(
2 -> { 2 -> {
ProjectSankeyDiagram( ProjectSankeyDiagram(
projectName = proj.name.ifEmpty { proj.remoteId }, projectName = proj.name.ifEmpty { proj.remoteId },
projectId = proj.id,
projectRemoteId = proj.remoteId,
allMembers = data.members, allMembers = data.members,
allBills = data.bills, allBills = data.bills,
customCategories = sankeyCategories, customCategories = sankeyCategories,
@@ -44,8 +44,8 @@ fun ProjectStatisticsTable(
val sdf = remember { SimpleDateFormat("yyyy-MM-dd", Locale.ROOT) } val sdf = remember { SimpleDateFormat("yyyy-MM-dd", Locale.ROOT) }
val dateFormat = remember { android.text.format.DateFormat.getDateFormat(context) } val dateFormat = remember { android.text.format.DateFormat.getDateFormat(context) }
var categoryId by remember { mutableIntStateOf(-1000) } var categoryId by remember { mutableLongStateOf(-1000L) }
var paymentModeId by remember { mutableIntStateOf(-1000) } var paymentModeId by remember { mutableLongStateOf(-1000L) }
var dateMin by remember { mutableStateOf<String?>(null) } var dateMin by remember { mutableStateOf<String?>(null) }
var dateMax by remember { mutableStateOf<String?>(null) } var dateMax by remember { mutableStateOf<String?>(null) }
@@ -61,10 +61,10 @@ fun ProjectStatisticsTable(
val shareStatsIntro = stringResource(R.string.msg_stats_intro, proj.name.ifEmpty { proj.remoteId }) val shareStatsIntro = stringResource(R.string.msg_stats_intro, proj.name.ifEmpty { proj.remoteId })
val categories = remember(proj.id, customCategories, categoryAll, categoryNone, categoryReimbursement, categoryAllExceptReimbursement) { val categories = remember(proj.id, customCategories, categoryAll, categoryNone, categoryReimbursement, categoryAllExceptReimbursement) {
val list = mutableListOf<Triple<Int, String, String>>() val list = mutableListOf<Triple<Long, String, String>>()
list.add(Triple(-1000, "📋", categoryAll)) list.add(Triple(-1000L, "📋", categoryAll))
list.add(Triple(-100, "🧾", categoryAllExceptReimbursement)) list.add(Triple(-100L, "🧾", categoryAllExceptReimbursement))
list.add(Triple(0, "", categoryNone)) list.add(Triple(0L, "", categoryNone))
val catsToUse = if (proj.type == ProjectType.LOCAL) { val catsToUse = if (proj.type == ProjectType.LOCAL) {
CategoryUtils.getDefaultCategories(context, proj.id) CategoryUtils.getDefaultCategories(context, proj.id)
@@ -75,15 +75,15 @@ fun ProjectStatisticsTable(
} }
catsToUse.forEach { catsToUse.forEach {
list.add(Triple(it.remoteId.toInt(), it.icon, it.name ?: "")) list.add(Triple(it.id, it.icon, it.name ?: ""))
} }
list.distinctBy { it.first } list.distinctBy { it.first }
} }
val paymentModes = remember(proj.id, customPaymentModes, paymentModeAll, paymentModeNone) { val paymentModes = remember(proj.id, customPaymentModes, paymentModeAll, paymentModeNone) {
val list = mutableListOf<Triple<Int, String, String>>() val list = mutableListOf<Triple<Long, String, String>>()
list.add(Triple(-1000, "💳", paymentModeAll)) list.add(Triple(-1000L, "💳", paymentModeAll))
list.add(Triple(0, "", paymentModeNone)) list.add(Triple(0L, "", paymentModeNone))
val pmsToUse = if (proj.type == ProjectType.LOCAL) { val pmsToUse = if (proj.type == ProjectType.LOCAL) {
CategoryUtils.getDefaultPaymentModes(context, proj.id) CategoryUtils.getDefaultPaymentModes(context, proj.id)
@@ -94,7 +94,7 @@ fun ProjectStatisticsTable(
} }
pmsToUse.forEach { pmsToUse.forEach {
list.add(Triple(it.remoteId.toInt(), it.icon, it.name ?: "")) list.add(Triple(it.id, it.icon, it.name ?: ""))
} }
list.distinctBy { it.first } list.distinctBy { it.first }
} }
@@ -105,10 +105,12 @@ fun ProjectStatisticsTable(
val membersPaid = HashMap<Long, Double>() val membersPaid = HashMap<Long, Double>()
val membersSpent = HashMap<Long, Double>() val membersSpent = HashMap<Long, Double>()
val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(customCategories, proj.id, proj.remoteId)
SupportUtil.getStats( SupportUtil.getStats(
allMembers, allBills, allMembers, allBills,
membersNbBills, membersBalance, membersPaid, membersSpent, membersNbBills, membersBalance, membersPaid, membersSpent,
categoryId, paymentModeId, dateMin, dateMax categoryId, paymentModeId, reimbursementCategoryId, dateMin, dateMax
) )
var statsText = shareStatsIntro + "\n\n" var statsText = shareStatsIntro + "\n\n"
@@ -1,6 +1,5 @@
package net.helcel.cowspent.model package net.helcel.cowspent.model
import android.util.Log
import java.io.Serializable import java.io.Serializable
import java.util.Calendar import java.util.Calendar
import java.util.Locale import java.util.Locale
@@ -16,9 +15,9 @@ open class DBBill(
var state: Int, var state: Int,
var repeat: String?, var repeat: String?,
var paymentMode: String?, var paymentMode: String?,
var categoryRemoteId: Int, var categoryId: Long,
var comment: String?, var comment: String?,
var paymentModeRemoteId: Int var paymentModeId: Long
) : Item, Serializable { ) : Item, Serializable {
var formattedWhat: String = "" var formattedWhat: String = ""
@@ -39,7 +38,6 @@ open class DBBill(
get() { get() {
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
cal.timeInMillis = timestamp * 1000 cal.timeInMillis = timestamp * 1000
Log.v("ll", "[$what] get date ts $timestamp year ${cal[Calendar.YEAR]}")
val month = cal[Calendar.MONTH] + 1 val month = cal[Calendar.MONTH] + 1
val day = cal[Calendar.DAY_OF_MONTH] val day = cal[Calendar.DAY_OF_MONTH]
return "${cal[Calendar.YEAR]}-${String.format(Locale.ROOT, "%02d", month)}-${ return "${cal[Calendar.YEAR]}-${String.format(Locale.ROOT, "%02d", month)}-${
@@ -58,7 +56,7 @@ open class DBBill(
override fun toString(): String { override fun toString(): String {
return "#DBBill$id/$remoteId,$projectId, $payerId, $amount, $timestamp, $what, $state, $repeat, $paymentMode, $categoryRemoteId" return "#DBBill$id/$remoteId,$projectId, $payerId, $amount, $timestamp, $what, $state, $repeat, $paymentMode, $categoryId"
} }
override fun isSection(): Boolean { override fun isSection(): Boolean {
@@ -73,15 +71,15 @@ open class DBBill(
const val PAYMODE_TRANSFER = "t" const val PAYMODE_TRANSFER = "t"
const val PAYMODE_ONLINE_SERVICE = "o" const val PAYMODE_ONLINE_SERVICE = "o"
const val PAYMODE_ID_NONE = 0 const val PAYMODE_ID_NONE = 0L
const val PAYMODE_ID_CARD = -1 const val PAYMODE_ID_CARD = -1L
const val PAYMODE_ID_CASH = -2 const val PAYMODE_ID_CASH = -2L
const val PAYMODE_ID_CHECK = -3 const val PAYMODE_ID_CHECK = -3L
const val PAYMODE_ID_TRANSFER = -4 const val PAYMODE_ID_TRANSFER = -4L
const val PAYMODE_ID_ONLINE_SERVICE = -5 const val PAYMODE_ID_ONLINE_SERVICE = -5L
@JvmField @JvmField
val oldPmIdToNew: Map<String, Int> = object : HashMap<String, Int>() { val oldPmIdToNew: Map<String, Long> = object : HashMap<String, Long>() {
init { init {
put(PAYMODE_NONE, PAYMODE_ID_NONE) put(PAYMODE_NONE, PAYMODE_ID_NONE)
put(PAYMODE_CARD, PAYMODE_ID_CARD) put(PAYMODE_CARD, PAYMODE_ID_CARD)
@@ -92,19 +90,19 @@ open class DBBill(
} }
} }
const val CATEGORY_NONE = 0 const val CATEGORY_NONE = 0L
const val CATEGORY_GROCERIES = -1 const val CATEGORY_GROCERIES = -1L
const val CATEGORY_LEISURE = -2 const val CATEGORY_LEISURE = -2L
const val CATEGORY_RENT = -3 const val CATEGORY_RENT = -3L
const val CATEGORY_BILLS = -4 const val CATEGORY_BILLS = -4L
const val CATEGORY_CULTURE = -5 const val CATEGORY_CULTURE = -5L
const val CATEGORY_HEALTH = -6 const val CATEGORY_HEALTH = -6L
const val CATEGORY_SHOPPING = -10 const val CATEGORY_SHOPPING = -10L
const val CATEGORY_REIMBURSEMENT = -11 const val CATEGORY_REIMBURSEMENT = -11L
const val CATEGORY_RESTAURANT = -12 const val CATEGORY_RESTAURANT = -12L
const val CATEGORY_ACCOMMODATION = -13 const val CATEGORY_ACCOMMODATION = -13L
const val CATEGORY_TRANSPORT = -14 const val CATEGORY_TRANSPORT = -14L
const val CATEGORY_SPORT = -15 const val CATEGORY_SPORT = -15L
const val STATE_OK = 0 const val STATE_OK = 0
const val STATE_ADDED = 1 const val STATE_ADDED = 1
@@ -8,7 +8,8 @@ class DBCategory(
var projectId: Long, var projectId: Long,
var name: String?, var name: String?,
var icon: String, var icon: String,
var color: String var color: String,
var state: Int = DBBill.STATE_OK
) : Serializable { ) : Serializable {
override fun toString(): String { override fun toString(): String {
@@ -8,7 +8,8 @@ class DBPaymentMode(
var projectId: Long, var projectId: Long,
var name: String?, var name: String?,
var icon: String, var icon: String,
var color: String var color: String,
var state: Int = DBBill.STATE_OK
) : Serializable { ) : Serializable {
override fun toString(): String { override fun toString(): String {
@@ -15,9 +15,9 @@ class GroupedBill(
sourceBills.first().state, sourceBills.first().state,
sourceBills.first().repeat, sourceBills.first().repeat,
sourceBills.first().paymentMode, sourceBills.first().paymentMode,
sourceBills.first().categoryRemoteId, sourceBills.first().categoryId,
sourceBills.first().comment, sourceBills.first().comment,
sourceBills.first().paymentModeRemoteId sourceBills.first().paymentModeId
), Serializable { ), Serializable {
init { init {
this.formattedWhat = sourceBills.first().formattedWhat this.formattedWhat = sourceBills.first().formattedWhat
File diff suppressed because it is too large Load Diff
@@ -265,6 +265,121 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val members = dbHelper.getMembersOfProject(project.id, null) val members = dbHelper.getMembersOfProject(project.id, null)
val memberIdToRemoteId = members.associate { it.id to it.remoteId } val memberIdToRemoteId = members.associate { it.id to it.remoteId }
val categoriesToAdd = dbHelper.getCategoriesOfProjectWithState(project.id, DBBill.STATE_ADDED)
for (catToAdd in categoriesToAdd) {
try {
val categoriesResponse = client!!.getCategories(project)
val remoteCategories = categoriesResponse.getCategories(project.id)
val matchingRemote = remoteCategories.find { it.name == catToAdd.name }
if (matchingRemote != null) {
dbHelper.updateCategory(catToAdd.id, null, null, null, DBBill.STATE_OK, matchingRemote.remoteId)
catToAdd.remoteId = matchingRemote.remoteId
} else {
val createResponse = client!!.createRemoteCategory(project, catToAdd)
val newRemoteId = createResponse.remoteCategoryId
if (newRemoteId > 0) {
dbHelper.updateCategory(catToAdd.id, null, null, null, DBBill.STATE_OK, newRemoteId)
catToAdd.remoteId = newRemoteId
}
}
} catch (e: Exception) {
Log.e(TAG, "CATEGORY SYNC FAILED for ${catToAdd.name}", e)
}
}
val categoriesToEdit = dbHelper.getCategoriesOfProjectWithState(project.id, DBBill.STATE_EDITED)
for (catToEdit in categoriesToEdit) {
try {
client!!.editRemoteCategory(project, catToEdit)
dbHelper.updateCategory(catToEdit.id, null, null, null, DBBill.STATE_OK)
} catch (e: Exception) {
Log.e(TAG, "EDIT CATEGORY FAILED for ${catToEdit.name}, might not exist remotely", e)
// If it fails, we keep the state as EDITED so it tries again next time,
// or we could set it to OK if we think it's a permanent mismatch.
// For now just log it.
}
}
val categoriesToDelete = dbHelper.getCategoriesOfProjectWithState(project.id, DBBill.STATE_DELETED)
for (catToDel in categoriesToDelete) {
try {
client!!.deleteRemoteCategory(project, catToDel.remoteId)
dbHelper.deleteCategory(catToDel.id)
} catch (e: NextcloudHttpRequestFailedException) {
if (e.statusCode == 404 || e.statusCode == 400) {
Log.d(TAG, "failed to delete category on remote project (code ${e.statusCode}) : delete it locally anyway")
dbHelper.deleteCategory(catToDel.id)
} else {
throw e
}
} catch (e: Exception) {
Log.e(TAG, "DELETE CATEGORY FAILED for ${catToDel.name}", e)
}
}
val paymentModesToAdd = dbHelper.getPaymentModesOfProjectWithState(project.id, DBBill.STATE_ADDED)
if (paymentModesToAdd.isNotEmpty()) {
try {
val pmsResponse = client!!.getPaymentModes(project)
val remotePms = pmsResponse.getPaymentModes(project.id)
val remotePmsNames = remotePms.map { it.name }
for (pmToAdd in paymentModesToAdd) {
val searchIndex = remotePmsNames.indexOf(pmToAdd.name)
if (searchIndex != -1) {
val remotePm = remotePms[searchIndex]
dbHelper.updatePaymentMode(pmToAdd.id, null, null, null, DBBill.STATE_OK, remotePm.remoteId)
pmToAdd.remoteId = remotePm.remoteId
} else {
val createRemotePaymentModeResponse = client!!.createRemotePaymentMode(project, pmToAdd)
val newRemoteId = createRemotePaymentModeResponse.remotePaymentModeId
if (newRemoteId > 0) {
dbHelper.updatePaymentMode(pmToAdd.id, null, null, null, DBBill.STATE_OK, newRemoteId)
pmToAdd.remoteId = newRemoteId
}
}
}
} catch (e: NextcloudHttpRequestFailedException) {
Log.e(TAG, "GET PAYMENT MODES FAILED : " + e.message)
}
}
val paymentModesToEdit = dbHelper.getPaymentModesOfProjectWithState(project.id, DBBill.STATE_EDITED)
for (pmToEdit in paymentModesToEdit) {
try {
client!!.editRemotePaymentMode(project, pmToEdit)
dbHelper.updatePaymentMode(pmToEdit.id, null, null, null, DBBill.STATE_OK)
} catch (e: Exception) {
Log.e(TAG, "EDIT PAYMENT MODE FAILED for ${pmToEdit.name}, might not exist remotely", e)
}
}
val paymentModesToDelete = dbHelper.getPaymentModesOfProjectWithState(project.id, DBBill.STATE_DELETED)
for (pmToDel in paymentModesToDelete) {
try {
client!!.deleteRemotePaymentMode(project, pmToDel.remoteId)
dbHelper.deletePaymentMode(pmToDel.id)
} catch (e: NextcloudHttpRequestFailedException) {
if (e.statusCode == 404 || e.statusCode == 400) {
Log.d(TAG, "failed to delete payment mode on remote project (code ${e.statusCode}) : delete it locally anyway")
dbHelper.deletePaymentMode(pmToDel.id)
} else {
throw e
}
} catch (_: Exception) {
}
}
val categories = dbHelper.getCategories(project.id)
val categoryIdToRemoteId = categories.associate { it.id to it.remoteId }.toMutableMap()
// Map hardcoded constants to themselves if not in DB
categoryIdToRemoteId[DBBill.CATEGORY_REIMBURSEMENT] = DBBill.CATEGORY_REIMBURSEMENT
categories.filter { it.remoteId < 0 }.forEach { categoryIdToRemoteId[it.remoteId] = it.remoteId }
val paymentModes = dbHelper.getPaymentModes(project.id)
val paymentModeIdToRemoteId = paymentModes.associate { it.id to it.remoteId }.toMutableMap()
paymentModes.filter { it.remoteId < 0 }.forEach { paymentModeIdToRemoteId[it.remoteId] = it.remoteId }
val toDelete = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_DELETED) val toDelete = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_DELETED)
for (bToDel in toDelete) { for (bToDel in toDelete) {
try { try {
@@ -293,12 +408,16 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val toEdit = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_EDITED) val toEdit = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_EDITED)
for (bToEdit in toEdit) { for (bToEdit in toEdit) {
try { try {
val editRemoteBillResponse = client!!.editRemoteBill(project, bToEdit, memberIdToRemoteId) val editRemoteBillResponse = client!!.editRemoteBill(project, bToEdit, memberIdToRemoteId, categoryIdToRemoteId, paymentModeIdToRemoteId)
if (editRemoteBillResponse.stringContent == bToEdit.remoteId.toString()) { val returnedRemoteId = editRemoteBillResponse.remoteBillId
if (returnedRemoteId == bToEdit.remoteId || (returnedRemoteId == 0L && !project.getRequestBaseUrl(true).contains("/ocs/v2.php"))) {
dbHelper.setBillState(bToEdit.id, DBBill.STATE_OK) dbHelper.setBillState(bToEdit.id, DBBill.STATE_OK)
Log.d(TAG, "SUCCESSFUL remote bill edition (${editRemoteBillResponse.stringContent})") Log.d(TAG, "SUCCESSFUL remote bill edition ($returnedRemoteId)")
} else if (returnedRemoteId > 0) {
dbHelper.setBillState(bToEdit.id, DBBill.STATE_OK)
Log.d(TAG, "SUCCESSFUL remote bill edition ($returnedRemoteId)")
} else { } else {
Log.d(TAG, "FAILED to edit remote bill (${editRemoteBillResponse.stringContent})") Log.d(TAG, "FAILED to edit remote bill ($returnedRemoteId)")
} }
} catch (_: Exception) { } catch (_: Exception) {
Log.d(TAG, "FAILED to edit remote bill: it probably does not exist remotely") Log.d(TAG, "FAILED to edit remote bill: it probably does not exist remotely")
@@ -307,8 +426,8 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val toAdd = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_ADDED) val toAdd = dbHelper.getBillsOfProjectWithState(project.id, DBBill.STATE_ADDED)
for (bToAdd in toAdd) { for (bToAdd in toAdd) {
val createRemoteBillResponse = client!!.createRemoteBill(project, bToAdd, memberIdToRemoteId) val createRemoteBillResponse = client!!.createRemoteBill(project, bToAdd, memberIdToRemoteId, categoryIdToRemoteId, paymentModeIdToRemoteId)
val newRemoteId = createRemoteBillResponse.stringContent.toLong() val newRemoteId = createRemoteBillResponse.remoteBillId
if (newRemoteId > 0) { if (newRemoteId > 0) {
dbHelper.updateBill( dbHelper.updateBill(
bToAdd.id, newRemoteId, null, bToAdd.id, newRemoteId, null,
@@ -375,7 +494,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val currencyToAdd = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_ADDED) val currencyToAdd = dbHelper.getCurrenciesOfProjectWithState(project.id, DBBill.STATE_ADDED)
for (cToAdd in currencyToAdd) { for (cToAdd in currencyToAdd) {
val createRemoteCurrencyResponse = client!!.createRemoteCurrency(project, cToAdd) val createRemoteCurrencyResponse = client!!.createRemoteCurrency(project, cToAdd)
val newRemoteId = createRemoteCurrencyResponse.stringContent.toLong() val newRemoteId = createRemoteCurrencyResponse.remoteCurrencyId
if (newRemoteId > 0) { if (newRemoteId > 0) {
dbHelper.setCurrencyState(cToAdd.id, DBBill.STATE_OK) dbHelper.setCurrencyState(cToAdd.id, DBBill.STATE_OK)
} }
@@ -471,7 +590,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val localPaymentModes = dbHelper.getPaymentModes(project.id) val localPaymentModes = dbHelper.getPaymentModes(project.id)
for (localPaymentMode in localPaymentModes) { for (localPaymentMode in localPaymentModes) {
if (!remotePaymentModesByRemoteId.containsKey(localPaymentMode.remoteId)) { if (localPaymentMode.state == DBBill.STATE_OK && !remotePaymentModesByRemoteId.containsKey(localPaymentMode.remoteId)) {
dbHelper.deletePaymentMode(localPaymentMode.id) dbHelper.deletePaymentMode(localPaymentMode.id)
Log.d(TAG, "Delete local pm : $localPaymentMode") Log.d(TAG, "Delete local pm : $localPaymentMode")
} }
@@ -481,6 +600,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val remoteCategoriesByRemoteId = remoteCategories.associateBy { it.remoteId } val remoteCategoriesByRemoteId = remoteCategories.associateBy { it.remoteId }
for (c in remoteCategories) { for (c in remoteCategories) {
if (c.remoteId == DBBill.CATEGORY_REIMBURSEMENT) continue
val localCategory = dbHelper.getCategory(c.remoteId, project.id) val localCategory = dbHelper.getCategory(c.remoteId, project.id)
if (localCategory == null) { if (localCategory == null) {
Log.d(TAG, "Add local category : $c") Log.d(TAG, "Add local category : $c")
@@ -500,7 +620,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val localCategories = dbHelper.getCategories(project.id) val localCategories = dbHelper.getCategories(project.id)
for (localCategory in localCategories) { for (localCategory in localCategories) {
if (!remoteCategoriesByRemoteId.containsKey(localCategory.remoteId)) { if (localCategory.state == DBBill.STATE_OK && !remoteCategoriesByRemoteId.containsKey(localCategory.remoteId)) {
dbHelper.deleteCategory(localCategory.id) dbHelper.deleteCategory(localCategory.id)
Log.d(TAG, "Delete local category : $localCategory") Log.d(TAG, "Delete local category : $localCategory")
} }
@@ -528,7 +648,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val localCurrencies = dbHelper.getCurrencies(project.id) val localCurrencies = dbHelper.getCurrencies(project.id)
for (localCurrency in localCurrencies) { for (localCurrency in localCurrencies) {
if (!remoteCurrenciesByRemoteId.containsKey(localCurrency.remoteId)) { if (localCurrency.state == DBBill.STATE_OK && !remoteCurrenciesByRemoteId.containsKey(localCurrency.remoteId)) {
dbHelper.deleteCurrency(localCurrency.id) dbHelper.deleteCurrency(localCurrency.id)
Log.d(TAG, "Delete local currency : $localCurrencies") Log.d(TAG, "Delete local currency : $localCurrencies")
} }
@@ -595,13 +715,23 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val dbMembers = dbHelper.getMembersOfProject(project.id, null) val dbMembers = dbHelper.getMembersOfProject(project.id, null)
val memberRemoteIdToId = dbMembers.associate { it.remoteId to it.id } val memberRemoteIdToId = dbMembers.associate { it.remoteId to it.id }
val dbCategories = dbHelper.getCategories(project.id)
val categoriesRemoteIdToId = dbCategories.associate { it.remoteId to it.id }.toMutableMap()
// Map hardcoded constants to their local IDs if they exist in DB, else to themselves
categoriesRemoteIdToId[DBBill.CATEGORY_REIMBURSEMENT] = DBBill.CATEGORY_REIMBURSEMENT
dbCategories.filter { it.remoteId < 0 }.forEach { categoriesRemoteIdToId[it.remoteId] = it.id }
val dbPaymentModes = dbHelper.getPaymentModes(project.id)
val paymentModesRemoteIdToId = dbPaymentModes.associate { it.remoteId to it.id }.toMutableMap()
dbPaymentModes.filter { it.remoteId < 0 }.forEach { paymentModesRemoteIdToId[it.remoteId] = it.id }
val billsResponse = client!!.getBills(project) val billsResponse = client!!.getBills(project)
val isIHM = project.type == ProjectType.IHATEMONEY val isIHM = project.type == ProjectType.IHATEMONEY
val serverSyncTimestamp = if (isIHM) 0L else billsResponse.syncTimestamp val serverSyncTimestamp = if (isIHM) 0L else billsResponse.syncTimestamp
val remoteBills: List<DBBill> = if (isIHM) { val remoteBills: List<DBBill> = if (isIHM) {
billsResponse.getBillsIHM(project.id, memberRemoteIdToId) billsResponse.getBillsIHM(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId)
} else { } else {
billsResponse.getBillsCospend(project.id, memberRemoteIdToId) billsResponse.getBillsCospend(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId)
} }
val remoteAllBillIds: List<Long> = if (isIHM) { val remoteAllBillIds: List<Long> = if (isIHM) {
remoteBills.map { it.remoteId } remoteBills.map { it.remoteId }
@@ -618,7 +748,6 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
dbHelper.addBill(remoteBill) dbHelper.addBill(remoteBill)
nbPulledNewBills++ nbPulledNewBills++
newBillsDialogText += "+ ${remoteBill.what}\n" newBillsDialogText += "+ ${remoteBill.what}\n"
Log.d(TAG, "Add local bill : $remoteBill")
} else { } else {
val localBill = localBillsByRemoteId[remoteBill.remoteId]!! val localBill = localBillsByRemoteId[remoteBill.remoteId]!!
if (hasChanged(localBill, remoteBill)) { if (hasChanged(localBill, remoteBill)) {
@@ -626,12 +755,11 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
localBill.id, null, remoteBill.payerId, localBill.id, null, remoteBill.payerId,
remoteBill.amount, remoteBill.timestamp, remoteBill.amount, remoteBill.timestamp,
remoteBill.what, DBBill.STATE_OK, remoteBill.repeat, remoteBill.what, DBBill.STATE_OK, remoteBill.repeat,
remoteBill.paymentMode, remoteBill.paymentModeRemoteId, remoteBill.paymentMode, remoteBill.paymentModeId,
remoteBill.categoryRemoteId, remoteBill.comment remoteBill.categoryId, remoteBill.comment
) )
nbPulledUpdatedBills++ nbPulledUpdatedBills++
updatedBillsDialogText += "${remoteBill.what}\n" updatedBillsDialogText += "${remoteBill.what}\n"
Log.d(TAG, "Update local bill : $remoteBill")
} else { } else {
Log.d(TAG, "Nothing to do for bill : $localBill") Log.d(TAG, "Nothing to do for bill : $localBill")
} }
@@ -1084,8 +1212,8 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
localBill.what == remoteBill.what && localBill.what == remoteBill.what &&
localBill.comment == remoteBill.comment && localBill.comment == remoteBill.comment &&
localBill.paymentMode == remoteBill.paymentMode && localBill.paymentMode == remoteBill.paymentMode &&
localBill.paymentModeRemoteId == remoteBill.paymentModeRemoteId && localBill.paymentModeId == remoteBill.paymentModeId &&
localBill.categoryRemoteId == remoteBill.categoryRemoteId localBill.categoryId == remoteBill.categoryId
) { ) {
val localRepeat = localBill.repeat ?: DBBill.NON_REPEATED val localRepeat = localBill.repeat ?: DBBill.NON_REPEATED
val remoteRepeat = remoteBill.repeat ?: DBBill.NON_REPEATED val remoteRepeat = remoteBill.repeat ?: DBBill.NON_REPEATED
@@ -14,11 +14,11 @@ object BillFormatter {
) { ) {
for (bill in bills) { for (bill in bills) {
var whatPrefix = "" var whatPrefix = ""
val pm = paymentModesMap[bill.paymentModeRemoteId.toLong()] val pm = paymentModesMap[bill.paymentModeId]
if (pm != null) { if (pm != null) {
whatPrefix += pm.icon + " " whatPrefix += pm.icon + " "
} else { } else {
when (bill.paymentModeRemoteId) { when (bill.paymentModeId) {
DBBill.PAYMODE_ID_CARD -> whatPrefix += "\uD83D\uDCB3 " DBBill.PAYMODE_ID_CARD -> whatPrefix += "\uD83D\uDCB3 "
DBBill.PAYMODE_ID_CASH -> whatPrefix += "\uD83D\uDCB5 " DBBill.PAYMODE_ID_CASH -> whatPrefix += "\uD83D\uDCB5 "
DBBill.PAYMODE_ID_CHECK -> whatPrefix += "\uD83C\uDFAB " DBBill.PAYMODE_ID_CHECK -> whatPrefix += "\uD83C\uDFAB "
@@ -27,11 +27,11 @@ object BillFormatter {
} }
} }
val cat = categoriesMap[bill.categoryRemoteId.toLong()] val cat = categoriesMap[bill.categoryId]
if (cat != null) { if (cat != null) {
whatPrefix += cat.icon + " " whatPrefix += cat.icon + " "
} else { } else {
when (bill.categoryRemoteId) { when (bill.categoryId) {
DBBill.CATEGORY_GROCERIES -> whatPrefix += "\uD83D\uDED2 " DBBill.CATEGORY_GROCERIES -> whatPrefix += "\uD83D\uDED2 "
DBBill.CATEGORY_LEISURE -> whatPrefix += "\uD83C\uDF89 " DBBill.CATEGORY_LEISURE -> whatPrefix += "\uD83C\uDF89 "
DBBill.CATEGORY_RENT -> whatPrefix += "\uD83C\uDFE0 " DBBill.CATEGORY_RENT -> whatPrefix += "\uD83C\uDFE0 "
@@ -10,28 +10,57 @@ object CategoryUtils {
fun getDefaultCategories(context: Context, projectId: Long): List<DBCategory> { fun getDefaultCategories(context: Context, projectId: Long): List<DBCategory> {
return listOf( return listOf(
DBCategory(0, DBBill.CATEGORY_GROCERIES.toLong(), projectId, context.getString(R.string.category_groceries), "\uD83D\uDED2", "#ffaa00"), DBCategory(DBBill.CATEGORY_GROCERIES,
DBCategory(0, DBBill.CATEGORY_LEISURE.toLong(), projectId, context.getString(R.string.category_leisure), "\uD83C\uDF89", "#aa55ff"), DBBill.CATEGORY_GROCERIES, projectId, context.getString(R.string.category_groceries), "\uD83D\uDED2", "#ffaa00"),
DBCategory(0, DBBill.CATEGORY_RENT.toLong(), projectId, context.getString(R.string.category_rent), "\uD83C\uDFE0", "#da8733"), DBCategory(DBBill.CATEGORY_LEISURE,
DBCategory(0, DBBill.CATEGORY_BILLS.toLong(), projectId, context.getString(R.string.category_bills), "\uD83C\uDF29", "#4aa6b0"), DBBill.CATEGORY_LEISURE, projectId, context.getString(R.string.category_leisure), "\uD83C\uDF89", "#aa55ff"),
DBCategory(0, DBBill.CATEGORY_CULTURE.toLong(), projectId, context.getString(R.string.category_excursion), "\uD83D\uDEB8", "#0055ff"), DBCategory(DBBill.CATEGORY_RENT,
DBCategory(0, DBBill.CATEGORY_HEALTH.toLong(), projectId, context.getString(R.string.category_health), "\uD83D\uDC9A", "#bf090c"), DBBill.CATEGORY_RENT, projectId, context.getString(R.string.category_rent), "\uD83C\uDFE0", "#da8733"),
DBCategory(0, DBBill.CATEGORY_SHOPPING.toLong(), projectId, context.getString(R.string.category_shopping), "\uD83D\uDECD", "#e167d1"), DBCategory(DBBill.CATEGORY_BILLS,
DBCategory(0, DBBill.CATEGORY_REIMBURSEMENT.toLong(), projectId, context.getString(R.string.category_reimbursement), "\uD83D\uDCB0", "#00ced1"), DBBill.CATEGORY_BILLS, projectId, context.getString(R.string.category_bills), "\uD83C\uDF29", "#4aa6b0"),
DBCategory(0, DBBill.CATEGORY_RESTAURANT.toLong(), projectId, context.getString(R.string.category_restaurant), "\uD83C\uDF74", "#d0d5e1"), DBCategory(DBBill.CATEGORY_CULTURE,
DBCategory(0, DBBill.CATEGORY_ACCOMMODATION.toLong(), projectId, context.getString(R.string.category_accomodation), "\uD83D\uDECC", "#5de1a3"), DBBill.CATEGORY_CULTURE, projectId, context.getString(R.string.category_excursion), "\uD83D\uDEB8", "#0055ff"),
DBCategory(0, DBBill.CATEGORY_TRANSPORT.toLong(), projectId, context.getString(R.string.category_transport), "\uD83D\uDE8C", "#6f2ee1"), DBCategory(DBBill.CATEGORY_HEALTH,
DBCategory(0, DBBill.CATEGORY_SPORT.toLong(), projectId, context.getString(R.string.category_sport), "\uD83C\uDFBE", "#69e177") DBBill.CATEGORY_HEALTH, projectId, context.getString(R.string.category_health), "\uD83D\uDC9A", "#bf090c"),
DBCategory(DBBill.CATEGORY_SHOPPING,
DBBill.CATEGORY_SHOPPING, projectId, context.getString(R.string.category_shopping), "\uD83D\uDECD", "#e167d1"),
DBCategory(DBBill.CATEGORY_REIMBURSEMENT,
DBBill.CATEGORY_REIMBURSEMENT, projectId, context.getString(R.string.category_reimbursement), "\uD83D\uDCB0", "#00ced1"),
DBCategory(DBBill.CATEGORY_RESTAURANT,
DBBill.CATEGORY_RESTAURANT, projectId, context.getString(R.string.category_restaurant), "\uD83C\uDF74", "#d0d5e1"),
DBCategory(DBBill.CATEGORY_ACCOMMODATION,
DBBill.CATEGORY_ACCOMMODATION, projectId, context.getString(R.string.category_accomodation), "\uD83D\uDECC", "#5de1a3"),
DBCategory(DBBill.CATEGORY_TRANSPORT,
DBBill.CATEGORY_TRANSPORT, projectId, context.getString(R.string.category_transport), "\uD83D\uDE8C", "#6f2ee1"),
DBCategory(DBBill.CATEGORY_SPORT,
DBBill.CATEGORY_SPORT, projectId, context.getString(R.string.category_sport), "\uD83C\uDFBE", "#69e177")
) )
} }
fun getDefaultPaymentModes(context: Context, projectId: Long): List<DBPaymentMode> { fun getDefaultPaymentModes(context: Context, projectId: Long): List<DBPaymentMode> {
return listOf( return listOf(
DBPaymentMode(0, DBBill.PAYMODE_ID_CARD.toLong(), projectId, context.getString(R.string.payment_mode_credit_card), "\uD83D\uDCB3", "#ff7f50"), DBPaymentMode(DBBill.PAYMODE_ID_CARD, DBBill.PAYMODE_ID_CARD, projectId, context.getString(R.string.payment_mode_credit_card), "\uD83D\uDCB3", "#ff7f50"),
DBPaymentMode(0, DBBill.PAYMODE_ID_CASH.toLong(), projectId, context.getString(R.string.payment_mode_cash), "\uD83D\uDCB5", "#556b2f"), DBPaymentMode(DBBill.PAYMODE_ID_CASH, DBBill.PAYMODE_ID_CASH, projectId, context.getString(R.string.payment_mode_cash), "\uD83D\uDCB5", "#556b2f"),
DBPaymentMode(0, DBBill.PAYMODE_ID_CHECK.toLong(), projectId, context.getString(R.string.payment_mode_check), "\uD83C\uDFAB", "#a9a9a9"), DBPaymentMode(DBBill.PAYMODE_ID_CHECK, DBBill.PAYMODE_ID_CHECK, projectId, context.getString(R.string.payment_mode_check), "\uD83C\uDFAB", "#a9a9a9"),
DBPaymentMode(0, DBBill.PAYMODE_ID_TRANSFER.toLong(), projectId, context.getString(R.string.payment_mode_transfer), "", "#00ced1"), DBPaymentMode(DBBill.PAYMODE_ID_TRANSFER, DBBill.PAYMODE_ID_TRANSFER, projectId, context.getString(R.string.payment_mode_transfer), "", "#00ced1"),
DBPaymentMode(0, DBBill.PAYMODE_ID_ONLINE_SERVICE.toLong(), projectId, context.getString(R.string.payment_mode_online), "\uD83C\uDF0E", "#9932cc") DBPaymentMode(DBBill.PAYMODE_ID_ONLINE_SERVICE, DBBill.PAYMODE_ID_ONLINE_SERVICE, projectId, context.getString(R.string.payment_mode_online), "\uD83C\uDF0E", "#9932cc")
) )
} }
fun getReimbursementCategoryId(categories: List<DBCategory>, projectId: Long, projectRemoteId: String? = null): Long {
return categories.find {
it.projectId == projectId && (
it.remoteId == DBBill.CATEGORY_REIMBURSEMENT ||
(projectRemoteId != null && it.remoteId.toString() == projectRemoteId)
)
}?.id ?: DBBill.CATEGORY_REIMBURSEMENT
}
fun getCategoryById(context: Context, id: Long, projectId: Long = 0): DBCategory? {
return getDefaultCategories(context, projectId).find { it.id == id }
}
fun getPaymentModeById(context: Context, id: Long, projectId: Long = 0): DBPaymentMode? {
return getDefaultPaymentModes(context, projectId).find { it.id == id }
}
} }
@@ -48,7 +48,7 @@ object ExportUtil {
} }
owersTxt = owersTxt.replace(",$".toRegex(), "") owersTxt = owersTxt.replace(",$".toRegex(), "")
fileContent += "\"${b.what}\",${b.amount},${b.date},${b.timestamp},\"$payerName\"," + fileContent += "\"${b.what}\",${b.amount},${b.date},${b.timestamp},\"$payerName\"," +
"$payerWeight,$payerActive,\"$owersTxt\",${b.repeat},${b.categoryRemoteId}," + "$payerWeight,$payerActive,\"$owersTxt\",${b.repeat},${b.categoryId}," +
"${b.paymentMode}\n" "${b.paymentMode}\n"
} }
@@ -93,6 +93,10 @@ class NextcloudClient(
params: Collection<QueryParam>?, params: Collection<QueryParam>?,
isOCSRequest: Boolean isOCSRequest: Boolean
): VersatileProjectSyncClient.ResponseData { ): VersatileProjectSyncClient.ResponseData {
var finalTarget = target
if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) {
finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json"
}
val result = StringBuilder() val result = StringBuilder()
val headers: MutableMap<String, List<String>> = HashMap() val headers: MutableMap<String, List<String>> = HashMap()
if (isOCSRequest) { if (isOCSRequest) {
@@ -103,13 +107,13 @@ class NextcloudClient(
val nextcloudRequest: NextcloudRequest = if (params == null) { val nextcloudRequest: NextcloudRequest = if (params == null) {
NextcloudRequest.Builder() NextcloudRequest.Builder()
.setMethod(method) .setMethod(method)
.setUrl(target) .setUrl(finalTarget)
.setHeader(headers) .setHeader(headers)
.build() .build()
} else { } else {
NextcloudRequest.Builder() NextcloudRequest.Builder()
.setMethod(method) .setMethod(method)
.setUrl(target) .setUrl(finalTarget)
.setParameter(params) .setParameter(params)
.setHeader(headers) .setHeader(headers)
.build() .build()
@@ -182,8 +186,12 @@ class NextcloudClient(
target: String, target: String,
method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean
): VersatileProjectSyncClient.ResponseData { ): VersatileProjectSyncClient.ResponseData {
var finalTarget = target
if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) {
finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json"
}
val result = StringBuilder() val result = StringBuilder()
val targetURL = url + target.replace("^/".toRegex(), "") val targetURL = url + finalTarget.replace("^/".toRegex(), "")
// Log.d(javaClass.simpleName, "method and target URL: $method $targetURL") // Log.d(javaClass.simpleName, "method and target URL: $method $targetURL")
val httpCon = SupportUtil.getHttpURLConnection(targetURL) val httpCon = SupportUtil.getHttpURLConnection(targetURL)
httpCon.requestMethod = method httpCon.requestMethod = method
@@ -194,7 +202,7 @@ class NextcloudClient(
) )
} }
httpCon.setRequestProperty("Connection", "Close") httpCon.setRequestProperty("Connection", "Close")
httpCon.setRequestProperty("User-Agent", "cowspent-android/" + SupportUtil.getAppVersionName(context)) httpCon.setRequestProperty("User-Agent", "Cowspent-android/" + SupportUtil.getAppVersionName(context))
if (lastETag != null && METHOD_GET == method) { if (lastETag != null && METHOD_GET == method) {
httpCon.setRequestProperty("If-None-Match", lastETag) httpCon.setRequestProperty("If-None-Match", lastETag)
} }
@@ -244,8 +252,12 @@ class NextcloudClient(
target: String, target: String,
method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean method: String, params: JSONObject?, lastETag: String?, needLogin: Boolean, isOCSRequest: Boolean
): VersatileProjectSyncClient.ResponseData { ): VersatileProjectSyncClient.ResponseData {
var finalTarget = target
if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) {
finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json"
}
var strBase64: String var strBase64: String
val targetURL = url + target.replace("^/".toRegex(), "") val targetURL = url + finalTarget.replace("^/".toRegex(), "")
val httpCon = SupportUtil.getHttpURLConnection( targetURL) val httpCon = SupportUtil.getHttpURLConnection( targetURL)
httpCon.requestMethod = method httpCon.requestMethod = method
if (needLogin) { if (needLogin) {
@@ -137,6 +137,90 @@ open class ServerResponse(
getResponseStringData().toLong() getResponseStringData().toLong()
} }
class CreateRemoteCategoryResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
) : ServerResponse(response, isOcsResponse) {
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
@get:Throws(JSONException::class)
val remoteCategoryId: Long
get() {
val dataStr = getResponseStringData()
return try {
dataStr.toLong()
} catch (_: NumberFormatException) {
val obj = JSONObject(dataStr)
obj.optLong("id", obj.optLong("remoteId", 0L))
}
}
}
class EditRemoteCategoryResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
) : ServerResponse(response, isOcsResponse) {
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
}
class DeleteRemoteCategoryResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
) : ServerResponse(response, isOcsResponse) {
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
}
class CreateRemotePaymentModeResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
) : ServerResponse(response, isOcsResponse) {
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
@get:Throws(JSONException::class)
val remotePaymentModeId: Long
get() {
val dataStr = getResponseStringData()
return try {
dataStr.toLong()
} catch (_: NumberFormatException) {
val obj = JSONObject(dataStr)
obj.optLong("id", obj.optLong("remoteId", 0L))
}
}
}
class EditRemotePaymentModeResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
) : ServerResponse(response, isOcsResponse) {
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
}
class DeleteRemotePaymentModeResponse(
response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean
) : ServerResponse(response, isOcsResponse) {
@get:Throws(JSONException::class)
val stringContent: String
get() = getResponseStringData()
}
class CreateRemoteCurrencyResponse( class CreateRemoteCurrencyResponse(
response: VersatileProjectSyncClient.ResponseData, response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean isOcsResponse: Boolean
@@ -145,6 +229,18 @@ open class ServerResponse(
@get:Throws(JSONException::class) @get:Throws(JSONException::class)
val stringContent: String val stringContent: String
get() = getResponseStringData() get() = getResponseStringData()
@get:Throws(JSONException::class)
val remoteCurrencyId: Long
get() {
val dataStr = getResponseStringData()
return try {
dataStr.toLong()
} catch (_: NumberFormatException) {
val obj = JSONObject(dataStr)
obj.optLong("id", obj.optLong("remoteId", 0L))
}
}
} }
class EditRemoteCurrencyResponse( class EditRemoteCurrencyResponse(
@@ -196,6 +292,18 @@ open class ServerResponse(
@get:Throws(JSONException::class) @get:Throws(JSONException::class)
val stringContent: String val stringContent: String
get() = getResponseStringData() get() = getResponseStringData()
@get:Throws(JSONException::class)
val remoteBillId: Long
get() {
val dataStr = getResponseStringData()
return try {
dataStr.toLong()
} catch (_: NumberFormatException) {
val obj = JSONObject(dataStr)
obj.optLong("id", obj.optLong("remoteId", 0L))
}
}
} }
class CreateRemoteBillResponse( class CreateRemoteBillResponse(
@@ -206,6 +314,18 @@ open class ServerResponse(
@get:Throws(JSONException::class) @get:Throws(JSONException::class)
val stringContent: String val stringContent: String
get() = getResponseStringData() get() = getResponseStringData()
@get:Throws(JSONException::class)
val remoteBillId: Long
get() {
val dataStr = getResponseStringData()
return try {
dataStr.toLong()
} catch (_: NumberFormatException) {
val obj = JSONObject(dataStr)
obj.optLong("id", obj.optLong("remoteId", 0L))
}
}
} }
class DeleteRemoteBillResponse( class DeleteRemoteBillResponse(
@@ -242,13 +362,23 @@ open class ServerResponse(
ServerResponse(response, isOcsResponse) { ServerResponse(response, isOcsResponse) {
@Throws(JSONException::class) @Throws(JSONException::class)
fun getBillsCospend(projId: Long, memberRemoteIdToId: Map<Long, Long>): List<DBBill> { fun getBillsCospend(
return getBillsFromJSONObject(getResponseObjectData(), projId, memberRemoteIdToId) projId: Long,
memberRemoteIdToId: Map<Long, Long>,
catRemoteIdToId: Map<Long, Long>,
pmRemoteIdToId: Map<Long, Long>
): List<DBBill> {
return getBillsFromJSONObject(projId, memberRemoteIdToId, catRemoteIdToId, pmRemoteIdToId)
} }
@Throws(JSONException::class) @Throws(JSONException::class)
fun getBillsIHM(projId: Long, memberRemoteIdToId: Map<Long, Long>): List<DBBill> { fun getBillsIHM(
return getBillsFromJSONArray(JSONArray(content), projId, memberRemoteIdToId) projId: Long,
memberRemoteIdToId: Map<Long, Long>,
catRemoteIdToId: Map<Long, Long>,
pmRemoteIdToId: Map<Long, Long>
): List<DBBill> {
return getBillsFromJSONArray(JSONArray(content), projId, memberRemoteIdToId, catRemoteIdToId, pmRemoteIdToId)
} }
@@ -270,6 +400,32 @@ open class ServerResponse(
} }
} }
class CategoriesResponse(response: VersatileProjectSyncClient.ResponseData, isOcsResponse: Boolean) :
ServerResponse(response, isOcsResponse) {
@Throws(JSONException::class)
fun getCategories(projId: Long): List<DBCategory> {
return if (isOcsResponse) {
getCategoriesFromJSON(getResponseObjectData(), projId)
} else {
getCategoriesFromJSONArray(getResponseArrayData(), projId)
}
}
}
class PaymentModesResponse(response: VersatileProjectSyncClient.ResponseData, isOcsResponse: Boolean) :
ServerResponse(response, isOcsResponse) {
@Throws(JSONException::class)
fun getPaymentModes(projId: Long): List<DBPaymentMode> {
return if (isOcsResponse) {
getPaymentModesFromJSON(getResponseObjectData(), projId)
} else {
getPaymentModesFromJSONArray(getResponseArrayData(), projId)
}
}
}
class AccountProjectsResponse( class AccountProjectsResponse(
response: VersatileProjectSyncClient.ResponseData, response: VersatileProjectSyncClient.ResponseData,
isOcsResponse: Boolean isOcsResponse: Boolean
@@ -366,6 +522,26 @@ open class ServerResponse(
return members return members
} }
@Throws(JSONException::class)
protected fun getCategoriesFromJSONArray(jsonCats: JSONArray, projId: Long): List<DBCategory> {
val categories: MutableList<DBCategory> = ArrayList()
for (i in 0 until jsonCats.length()) {
val jsonCat = jsonCats.getJSONObject(i)
categories.add(getCategoryFromJSON(jsonCat, projId))
}
return categories
}
@Throws(JSONException::class)
protected fun getPaymentModesFromJSONArray(jsonPms: JSONArray, projId: Long): List<DBPaymentMode> {
val paymentModes: MutableList<DBPaymentMode> = ArrayList()
for (i in 0 until jsonPms.length()) {
val jsonPm = jsonPms.getJSONObject(i)
paymentModes.add(getPaymentModeFromJSON(jsonPm, projId))
}
return paymentModes
}
@Throws(JSONException::class) @Throws(JSONException::class)
protected fun getCategoriesFromJSON(json: JSONObject, projId: Long): List<DBCategory> { protected fun getCategoriesFromJSON(json: JSONObject, projId: Long): List<DBCategory> {
val categories: MutableList<DBCategory> = ArrayList() val categories: MutableList<DBCategory> = ArrayList()
@@ -382,6 +558,27 @@ open class ServerResponse(
return categories return categories
} }
@Throws(JSONException::class)
protected fun getCategoryFromJSON(json: JSONObject, projId: Long): DBCategory {
var remoteId: Long = 0
if (json.has("id") && !json.isNull("id")) {
remoteId = json.getLong("id")
}
var name = ""
var color = ""
var icon = ""
if (json.has("color") && !json.isNull("color")) {
color = json.getString("color")
}
if (json.has("icon") && !json.isNull("icon")) {
icon = json.getString("icon")
}
if (json.has("name") && !json.isNull("name")) {
name = json.getString("name")
}
return DBCategory(0, remoteId, projId, name, icon, color)
}
@Throws(JSONException::class) @Throws(JSONException::class)
protected fun getCategoryFromJSON(json: JSONObject, remoteIdStr: String, projId: Long): DBCategory { protected fun getCategoryFromJSON(json: JSONObject, remoteIdStr: String, projId: Long): DBCategory {
val remoteId = remoteIdStr.toLong() val remoteId = remoteIdStr.toLong()
@@ -416,6 +613,27 @@ open class ServerResponse(
return paymentModes return paymentModes
} }
@Throws(JSONException::class)
protected fun getPaymentModeFromJSON(json: JSONObject, projId: Long): DBPaymentMode {
var remoteId: Long = 0
if (json.has("id") && !json.isNull("id")) {
remoteId = json.getLong("id")
}
var name = ""
var color = ""
var icon = ""
if (json.has("color") && !json.isNull("color")) {
color = json.getString("color")
}
if (json.has("icon") && !json.isNull("icon")) {
icon = json.getString("icon")
}
if (json.has("name") && !json.isNull("name")) {
name = json.getString("name")
}
return DBPaymentMode(0, remoteId, projId, name, icon, color)
}
@Throws(JSONException::class) @Throws(JSONException::class)
protected fun getPaymentModeFromJSON(json: JSONObject, remoteIdStr: String, projId: Long): DBPaymentMode { protected fun getPaymentModeFromJSON(json: JSONObject, remoteIdStr: String, projId: Long): DBPaymentMode {
val remoteId = remoteIdStr.toLong() val remoteId = remoteIdStr.toLong()
@@ -556,26 +774,30 @@ open class ServerResponse(
protected fun getBillsFromJSONArray( protected fun getBillsFromJSONArray(
json: JSONArray, json: JSONArray,
projId: Long, projId: Long,
memberRemoteIdToId: Map<Long, Long> memberRemoteIdToId: Map<Long, Long>,
catRemoteIdToId: Map<Long, Long>,
pmRemoteIdToId: Map<Long, Long>
): List<DBBill> { ): List<DBBill> {
val bills: MutableList<DBBill> = ArrayList() val bills: MutableList<DBBill> = ArrayList()
for (i in 0 until json.length()) { for (i in 0 until json.length()) {
val jsonBill = json.getJSONObject(i) val jsonBill = json.getJSONObject(i)
bills.add(getBillFromJSON(jsonBill, projId, memberRemoteIdToId)) bills.add(getBillFromJSON(jsonBill, projId, memberRemoteIdToId, catRemoteIdToId, pmRemoteIdToId))
} }
return bills return bills
} }
@Throws(JSONException::class) @Throws(JSONException::class)
protected fun getBillsFromJSONObject( protected fun getBillsFromJSONObject(
json: JSONObject,
projId: Long, projId: Long,
memberRemoteIdToId: Map<Long, Long> memberRemoteIdToId: Map<Long, Long>,
catRemoteIdToId: Map<Long, Long>,
pmRemoteIdToId: Map<Long, Long>
): List<DBBill> { ): List<DBBill> {
val bills: List<DBBill> val bills: List<DBBill>
val json = getResponseObjectData()
if (json.has("bills") && !json.isNull("bills")) { if (json.has("bills") && !json.isNull("bills")) {
val jsonBills = json.getJSONArray("bills") val jsonBills = json.getJSONArray("bills")
bills = getBillsFromJSONArray(jsonBills, projId, memberRemoteIdToId) bills = getBillsFromJSONArray(jsonBills, projId, memberRemoteIdToId, catRemoteIdToId, pmRemoteIdToId)
} else { } else {
bills = ArrayList() bills = ArrayList()
} }
@@ -586,7 +808,9 @@ open class ServerResponse(
protected fun getBillFromJSON( protected fun getBillFromJSON(
json: JSONObject, json: JSONObject,
projId: Long, projId: Long,
memberRemoteIdToId: Map<Long, Long> memberRemoteIdToId: Map<Long, Long>,
catRemoteIdToId: Map<Long, Long>,
pmRemoteIdToId: Map<Long, Long>
): DBBill { ): DBBill {
var remoteId: Long = 0 var remoteId: Long = 0
var payerRemoteId: Long var payerRemoteId: Long
@@ -600,7 +824,7 @@ open class ServerResponse(
var repeat = DBBill.NON_REPEATED var repeat = DBBill.NON_REPEATED
var paymentMode = DBBill.PAYMODE_NONE var paymentMode = DBBill.PAYMODE_NONE
var paymentModeRemoteId = DBBill.PAYMODE_ID_NONE var paymentModeRemoteId = DBBill.PAYMODE_ID_NONE
var categoryId = DBBill.CATEGORY_NONE var categoryRemoteId = DBBill.CATEGORY_NONE
if (!json.isNull("id")) { if (!json.isNull("id")) {
remoteId = json.getLong("id") remoteId = json.getLong("id")
} }
@@ -639,23 +863,29 @@ open class ServerResponse(
} }
if (json.has("paymentmode") && !json.isNull("paymentmode")) { if (json.has("paymentmode") && !json.isNull("paymentmode")) {
paymentMode = json.getString("paymentmode") paymentMode = json.getString("paymentmode")
} else if (json.has("paymentMode") && !json.isNull("paymentMode")) {
paymentMode = json.getString("paymentMode")
} }
if (json.has("categoryid") && !json.isNull("categoryid")) { if (json.has("categoryid") && !json.isNull("categoryid")) {
categoryId = json.getInt("categoryid") categoryRemoteId = json.getLong("categoryid")
Log.d("PLOP", "LOADED CATTTTTTTTTTTT $categoryId") } else if (json.has("categoryId") && !json.isNull("categoryId")) {
categoryRemoteId = json.getLong("categoryId")
} }
if (json.has("paymentmodeid") && !json.isNull("paymentmodeid")) { if (json.has("paymentmodeid") && !json.isNull("paymentmodeid")) {
paymentModeRemoteId = json.getInt("paymentmodeid") paymentModeRemoteId = json.getLong("paymentmodeid")
} else if (json.has("paymentModeId") && !json.isNull("paymentModeId")) {
paymentModeRemoteId = json.getLong("paymentModeId")
} }
// old MB, new Cospend is ok as Cospend provides the old pm ID
// new MB, old Cospend => set payment mode ID from old one
if (DBBill.PAYMODE_NONE != paymentMode && "" != paymentMode && paymentModeRemoteId == DBBill.PAYMODE_ID_NONE) { if (DBBill.PAYMODE_NONE != paymentMode && "" != paymentMode && paymentModeRemoteId == DBBill.PAYMODE_ID_NONE) {
Log.d("PaymentMode", "old: $paymentMode and new: 0")
paymentModeRemoteId = DBBill.oldPmIdToNew[paymentMode] ?: DBBill.PAYMODE_ID_NONE paymentModeRemoteId = DBBill.oldPmIdToNew[paymentMode] ?: DBBill.PAYMODE_ID_NONE
} }
val categoryId = catRemoteIdToId[categoryRemoteId] ?: 0L
val paymentModeId = pmRemoteIdToId[paymentModeRemoteId] ?: 0L
val bill = DBBill( val bill = DBBill(
0, remoteId, projId, payerId, amount, timestamp, what, 0, remoteId, projId, payerId, amount, timestamp, what,
DBBill.STATE_OK, repeat, paymentMode, categoryId, comment, paymentModeRemoteId DBBill.STATE_OK, repeat, paymentMode, categoryId, comment, paymentModeId
) )
bill.billOwers = getBillOwersFromJson(json, memberRemoteIdToId) bill.billOwers = getBillOwersFromJson(json, memberRemoteIdToId)
return bill return bill
@@ -87,14 +87,17 @@ object SupportUtil {
membersBalance: MutableMap<Long, Double>, membersBalance: MutableMap<Long, Double>,
membersPaid: MutableMap<Long, Double>, membersPaid: MutableMap<Long, Double>,
membersSpent: MutableMap<Long, Double>, membersSpent: MutableMap<Long, Double>,
catId: Int, paymentModeId: Int, catId: Long, paymentModeId: Long,
dateMin: String?, dateMax: String? dateMin: String?, dateMax: String?
): Int { ): Int {
val proj = db.getProject(projId)
val categories = db.getCategories(projId)
val reimbursementCategoryId = CategoryUtils.getReimbursementCategoryId(categories, projId, proj?.remoteId)
return getStats( return getStats(
db.getMembersOfProject(projId, null), db.getMembersOfProject(projId, null),
db.getBillsOfProject(projId), db.getBillsOfProject(projId),
membersNbBills, membersBalance, membersPaid, membersSpent, membersNbBills, membersBalance, membersPaid, membersSpent,
catId, paymentModeId, dateMin, dateMax catId, paymentModeId, reimbursementCategoryId, dateMin, dateMax
) )
} }
@@ -106,7 +109,8 @@ object SupportUtil {
membersBalance: MutableMap<Long, Double>, membersBalance: MutableMap<Long, Double>,
membersPaid: MutableMap<Long, Double>, membersPaid: MutableMap<Long, Double>,
membersSpent: MutableMap<Long, Double>, membersSpent: MutableMap<Long, Double>,
catId: Int, paymentModeId: Int, catId: Long, paymentModeId: Long,
reimbursementCategoryId: Long,
dateMin: String?, dateMax: String? dateMin: String?, dateMax: String?
): Int { ): Int {
val nbBillsTotal = 0 val nbBillsTotal = 0
@@ -124,9 +128,9 @@ object SupportUtil {
for (b in dbBills) { for (b in dbBills) {
// don't take deleted bills and respect category filter // don't take deleted bills and respect category filter
if (b.state != DBBill.STATE_DELETED && if (b.state != DBBill.STATE_DELETED &&
((catId == -1000 || catId == -100 || b.categoryRemoteId == catId) && ((catId == -1000L || catId == -100L || b.categoryId == catId) &&
(catId != -100 || b.categoryRemoteId != DBBill.CATEGORY_REIMBURSEMENT) && (catId != -100L || b.categoryId != reimbursementCategoryId) &&
(paymentModeId == -1000 || b.paymentModeRemoteId == paymentModeId)) && (paymentModeId == -1000L || b.paymentModeId == paymentModeId)) &&
(dateMin == null || b.date >= dateMin) && (dateMin == null || b.date >= dateMin) &&
(dateMax == null || b.date <= dateMax) (dateMax == null || b.date <= dateMax)
) { ) {
@@ -285,18 +289,6 @@ object SupportUtil {
return reduceBalance(crediters, debiters, results) return reduceBalance(crediters, debiters, results)
} }
@JvmStatic
fun getVersionName(context: Context): String {
var versionName = "0.0.0"
try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
versionName = pInfo.versionName ?: "0.0.0"
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
return versionName
}
@JvmStatic @JvmStatic
fun getJsonObject(text: String?): JSONObject? { fun getJsonObject(text: String?): JSONObject? {
if (text == null) return null if (text == null) return null
@@ -11,8 +11,10 @@ import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException
import com.nextcloud.android.sso.exceptions.TokenMismatchException import com.nextcloud.android.sso.exceptions.TokenMismatchException
import com.nextcloud.android.sso.model.SingleSignOnAccount import com.nextcloud.android.sso.model.SingleSignOnAccount
import net.helcel.cowspent.model.DBBill import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBCategory
import net.helcel.cowspent.model.DBCurrency import net.helcel.cowspent.model.DBCurrency
import net.helcel.cowspent.model.DBMember import net.helcel.cowspent.model.DBMember
import net.helcel.cowspent.model.DBPaymentMode
import net.helcel.cowspent.model.DBProject import net.helcel.cowspent.model.DBProject
import net.helcel.cowspent.model.ProjectType import net.helcel.cowspent.model.ProjectType
import org.json.JSONException import org.json.JSONException
@@ -82,13 +84,11 @@ class VersatileProjectSyncClient(
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId
useOcsApiRequest = cospendVersionGT161 useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) { } else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) { target = if (cospendVersionGT161)
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
ServerResponse.ProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, true), true) else
} else { "/index.php/apps/cospend/api-priv/projects/" + project.remoteId
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId return ServerResponse.ProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, cospendVersionGT161), cospendVersionGT161)
ServerResponse.ProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, false), false)
}
} else { } else {
useOcsApiRequest = cospendVersionGT161 useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161) target = if (cospendVersionGT161)
@@ -260,7 +260,13 @@ class VersatileProjectSyncClient(
} }
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun editRemoteBill(project: DBProject, bill: DBBill, memberIdToRemoteId: Map<Long, Long>): ServerResponse.EditRemoteBillResponse { fun editRemoteBill(
project: DBProject,
bill: DBBill,
memberIdToRemoteId: Map<Long, Long>,
categoryIdToRemoteId: Map<Long, Long>,
paymentModeIdToRemoteId: Map<Long, Long>
): ServerResponse.EditRemoteBillResponse {
val paramKeys: MutableList<String> = ArrayList() val paramKeys: MutableList<String> = ArrayList()
val paramValues: MutableList<String> = ArrayList() val paramValues: MutableList<String> = ArrayList()
paramKeys.add("date") paramKeys.add("date")
@@ -307,8 +313,8 @@ class VersatileProjectSyncClient(
payedFor = payedFor.replace(",$".toRegex(), "") payedFor = payedFor.replace(",$".toRegex(), "")
paramValues.add(payedFor) paramValues.add(payedFor)
paramValues.add(bill.paymentMode ?: "") paramValues.add(bill.paymentMode ?: "")
paramValues.add(bill.categoryRemoteId.toString()) paramValues.add((categoryIdToRemoteId[bill.categoryId] ?: 0L).toString())
paramValues.add(bill.paymentModeRemoteId.toString()) paramValues.add((paymentModeIdToRemoteId[bill.paymentModeId] ?: 0L).toString())
if (canAccessProjectWithNCLogin(project)) { if (canAccessProjectWithNCLogin(project)) {
username = this.username username = this.username
@@ -512,7 +518,13 @@ class VersatileProjectSyncClient(
} }
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun createRemoteBill(project: DBProject, bill: DBBill, memberIdToRemoteId: Map<Long, Long>): ServerResponse.CreateRemoteBillResponse { fun createRemoteBill(
project: DBProject,
bill: DBBill,
memberIdToRemoteId: Map<Long, Long>,
categoryIdToRemoteId: Map<Long, Long>,
paymentModeIdToRemoteId: Map<Long, Long>
): ServerResponse.CreateRemoteBillResponse {
val paramKeys: MutableList<String> = ArrayList() val paramKeys: MutableList<String> = ArrayList()
val paramValues: MutableList<String> = ArrayList() val paramValues: MutableList<String> = ArrayList()
paramKeys.add("date") paramKeys.add("date")
@@ -554,8 +566,8 @@ class VersatileProjectSyncClient(
payedFor = payedFor.replace(",$".toRegex(), "") payedFor = payedFor.replace(",$".toRegex(), "")
paramValues.add(payedFor) paramValues.add(payedFor)
paramValues.add(bill.paymentMode ?: "") paramValues.add(bill.paymentMode ?: "")
paramValues.add(bill.categoryRemoteId.toString()) paramValues.add((categoryIdToRemoteId[bill.categoryId] ?: 0L).toString())
paramValues.add(bill.paymentModeRemoteId.toString()) paramValues.add((paymentModeIdToRemoteId[bill.paymentModeId] ?: 0L).toString())
if (canAccessProjectWithNCLogin(project)) { if (canAccessProjectWithNCLogin(project)) {
username = this.username username = this.username
@@ -634,7 +646,6 @@ class VersatileProjectSyncClient(
} else if (canAccessProjectWithSSO(project)) { } else if (canAccessProjectWithSSO(project)) {
return if (cospendVersionGT161) { return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/members" target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/members"
Log.i(TAG, "using new API for createRemoteBill")
ServerResponse.CreateRemoteMemberResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, true), isOcsResponse=true, isJsonMember=true) ServerResponse.CreateRemoteMemberResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, true), isOcsResponse=true, isJsonMember=true)
} else { } else {
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/members" target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/members"
@@ -698,7 +709,6 @@ class VersatileProjectSyncClient(
paramValues.add(tsLastSync.toString()) paramValues.add(tsLastSync.toString())
return if (cospendVersionGT161) { return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills" target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills"
Log.i(TAG, "using new API for getBills")
ServerResponse.BillsResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, paramKeys, paramValues, true), true) ServerResponse.BillsResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, paramKeys, paramValues, true), true)
} else { } else {
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/bills" target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/bills"
@@ -733,6 +743,92 @@ class VersatileProjectSyncClient(
} }
} }
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun getCategories(project: DBProject): ServerResponse.CategoriesResponse {
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/categories"
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/categories"
return ServerResponse.CategoriesResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/categories"
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/categories"
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
return ServerResponse.CategoriesResponse(
requestServer(
target, METHOD_GET, null, null,
null, username, password, bearerToken, useOcsApiRequest
), useOcsApiRequest
)
}
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun getPaymentModes(project: DBProject): ServerResponse.PaymentModesResponse {
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/paymentmodes"
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/paymentmodes"
return ServerResponse.PaymentModesResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmodes"
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/paymentmodes"
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
return ServerResponse.PaymentModesResponse(
requestServer(
target, METHOD_GET, null, null,
null, username, password, bearerToken, useOcsApiRequest
), useOcsApiRequest
)
}
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) @Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun getMembers(project: DBProject): ServerResponse.MembersResponse { fun getMembers(project: DBProject): ServerResponse.MembersResponse {
var target: String var target: String
@@ -779,6 +875,305 @@ class VersatileProjectSyncClient(
) )
} }
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun createRemoteCategory(project: DBProject, category: DBCategory): ServerResponse.CreateRemoteCategoryResponse {
val paramKeys: MutableList<String> = ArrayList()
val paramValues: MutableList<String> = ArrayList()
paramKeys.add("name")
paramValues.add(category.name ?: "")
paramKeys.add("icon")
paramValues.add(category.icon)
paramKeys.add("color")
paramValues.add(category.color)
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/category"
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/categories"
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/category"
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/categories"
return ServerResponse.CreateRemoteCategoryResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/category"
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/categories"
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/categories"
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
val response = requestServer(
target, METHOD_POST, paramKeys, paramValues, null,
username, password, bearerToken, useOcsApiRequest
)
return ServerResponse.CreateRemoteCategoryResponse(response, useOcsApiRequest)
}
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun editRemoteCategory(project: DBProject, category: DBCategory): ServerResponse.EditRemoteCategoryResponse {
val paramKeys: MutableList<String> = ArrayList()
val paramValues: MutableList<String> = ArrayList()
paramKeys.add("name")
paramValues.add(category.name ?: "")
paramKeys.add("icon")
paramValues.add(category.icon)
paramKeys.add("color")
paramValues.add(category.color)
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/category/" + category.remoteId
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/categories/" + category.remoteId
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/category/" + category.remoteId
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/categories/" + category.remoteId
return ServerResponse.EditRemoteCategoryResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/category/" + category.remoteId
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/categories/" + category.remoteId
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/categories/" + category.remoteId
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
return ServerResponse.EditRemoteCategoryResponse(
requestServer(
target, METHOD_PUT, paramKeys, paramValues, null,
username, password, bearerToken, useOcsApiRequest
), useOcsApiRequest
)
}
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun deleteRemoteCategory(project: DBProject, categoryRemoteId: Long): ServerResponse.DeleteRemoteCategoryResponse {
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/category/" + categoryRemoteId
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/categories/" + categoryRemoteId
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/category/" + categoryRemoteId
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/categories/" + categoryRemoteId
return ServerResponse.DeleteRemoteCategoryResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_DELETE, null, null, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/category/" + categoryRemoteId
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/categories/" + categoryRemoteId
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/categories/" + categoryRemoteId
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
return ServerResponse.DeleteRemoteCategoryResponse(
requestServer(
target, METHOD_DELETE, null, null,
null, username, password, bearerToken, useOcsApiRequest
), useOcsApiRequest
)
}
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun createRemotePaymentMode(project: DBProject, paymentMode: DBPaymentMode): ServerResponse.CreateRemotePaymentModeResponse {
val paramKeys: MutableList<String> = ArrayList()
val paramValues: MutableList<String> = ArrayList()
paramKeys.add("name")
paramValues.add(paymentMode.name ?: "")
paramKeys.add("icon")
paramValues.add(paymentMode.icon)
paramKeys.add("color")
paramValues.add(paymentMode.color)
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/paymentmode"
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/paymentmodes"
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/paymentmode"
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/paymentmodes"
return ServerResponse.CreateRemotePaymentModeResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_POST, paramKeys, paramValues, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmode"
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmodes"
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/paymentmodes"
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
return ServerResponse.CreateRemotePaymentModeResponse(
requestServer(
target, METHOD_POST, paramKeys, paramValues, null,
username, password, bearerToken, useOcsApiRequest
), useOcsApiRequest
)
}
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun editRemotePaymentMode(project: DBProject, paymentMode: DBPaymentMode): ServerResponse.EditRemotePaymentModeResponse {
val paramKeys: MutableList<String> = ArrayList()
val paramValues: MutableList<String> = ArrayList()
paramKeys.add("name")
paramValues.add(paymentMode.name ?: "")
paramKeys.add("icon")
paramValues.add(paymentMode.icon)
paramKeys.add("color")
paramValues.add(paymentMode.color)
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/paymentmode/" + paymentMode.remoteId
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/paymentmodes/" + paymentMode.remoteId
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/paymentmode/" + paymentMode.remoteId
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/paymentmodes/" + paymentMode.remoteId
return ServerResponse.EditRemotePaymentModeResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmode/" + paymentMode.remoteId
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmodes/" + paymentMode.remoteId
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/paymentmodes/" + paymentMode.remoteId
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
return ServerResponse.EditRemotePaymentModeResponse(
requestServer(
target, METHOD_PUT, paramKeys, paramValues, null,
username, password, bearerToken, useOcsApiRequest
), useOcsApiRequest
)
}
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun deleteRemotePaymentMode(project: DBProject, paymentModeRemoteId: Long): ServerResponse.DeleteRemotePaymentModeResponse {
var target: String
var username: String? = null
var password: String? = null
var bearerToken: String? = null
var useOcsApiRequest = false
if (ProjectType.COSPEND == project.type) {
if (canAccessProjectWithNCLogin(project)) {
username = this.username
password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/paymentmode/" + paymentModeRemoteId
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/paymentmodes/" + paymentModeRemoteId
useOcsApiRequest = cospendVersionGT161
} else if (canAccessProjectWithSSO(project)) {
target = if (cospendVersionGT161)
"/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/paymentmode/" + paymentModeRemoteId
else
"/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/paymentmodes/" + paymentModeRemoteId
return ServerResponse.DeleteRemotePaymentModeResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_DELETE, null, null, cospendVersionGT161), cospendVersionGT161)
} else {
useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmode/" + paymentModeRemoteId
else
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/paymentmodes/" + paymentModeRemoteId
}
} else {
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/paymentmodes/" + paymentModeRemoteId
username = project.remoteId
password = project.password
bearerToken = project.bearerToken
}
return ServerResponse.DeleteRemotePaymentModeResponse(
requestServer(
target, METHOD_DELETE, null, null,
null, username, password, bearerToken, useOcsApiRequest
), useOcsApiRequest
)
}
@Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) @Throws(IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun createRemoteCurrency(project: DBProject, currency: DBCurrency): ServerResponse.CreateRemoteCurrencyResponse { fun createRemoteCurrency(project: DBProject, currency: DBCurrency): ServerResponse.CreateRemoteCurrencyResponse {
val paramKeys: MutableList<String> = ArrayList() val paramKeys: MutableList<String> = ArrayList()
@@ -936,6 +1331,10 @@ class VersatileProjectSyncClient(
nextcloudAPI: NextcloudAPI, target: String, method: String, nextcloudAPI: NextcloudAPI, target: String, method: String,
paramKeys: List<String>?, paramValues: List<String>?, isOCSRequest: Boolean paramKeys: List<String>?, paramValues: List<String>?, isOCSRequest: Boolean
): ResponseData { ): ResponseData {
var finalTarget = target
if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) {
finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json"
}
val result = StringBuilder() val result = StringBuilder()
var params: MutableList<QueryParam>? = null var params: MutableList<QueryParam>? = null
if (paramKeys != null && paramValues != null) { if (paramKeys != null && paramValues != null) {
@@ -953,13 +1352,13 @@ class VersatileProjectSyncClient(
val nextcloudRequest: NextcloudRequest = if (params == null) { val nextcloudRequest: NextcloudRequest = if (params == null) {
NextcloudRequest.Builder() NextcloudRequest.Builder()
.setMethod(method) .setMethod(method)
.setUrl(target) .setUrl(finalTarget)
.setHeader(headers) .setHeader(headers)
.build() .build()
} else { } else {
NextcloudRequest.Builder() NextcloudRequest.Builder()
.setMethod(method) .setMethod(method)
.setUrl(target) .setUrl(finalTarget)
.setParameter(params) .setParameter(params)
.setHeader(headers) .setHeader(headers)
.build() .build()
@@ -993,8 +1392,12 @@ class VersatileProjectSyncClient(
lastETag: String?, username: String?, password: String?, lastETag: String?, username: String?, password: String?,
bearerToken: String?, isOCSRequest: Boolean bearerToken: String?, isOCSRequest: Boolean
): ResponseData { ): ResponseData {
var finalTarget = target
if (finalTarget.contains("/ocs/v2.php") && !finalTarget.contains("format=json")) {
finalTarget += if (finalTarget.contains("?")) "&format=json" else "?format=json"
}
val result = StringBuilder() val result = StringBuilder()
val httpCon = SupportUtil.getHttpURLConnection(target) val httpCon = SupportUtil.getHttpURLConnection(finalTarget)
httpCon.requestMethod = method httpCon.requestMethod = method
if (bearerToken != null) { if (bearerToken != null) {
httpCon.setRequestProperty("Authorization", "Bearer $bearerToken") httpCon.setRequestProperty("Authorization", "Bearer $bearerToken")
@@ -1005,7 +1408,7 @@ class VersatileProjectSyncClient(
) )
} }
httpCon.setRequestProperty("Connection", "Close") httpCon.setRequestProperty("Connection", "Close")
httpCon.setRequestProperty("User-Agent", "Cowspent/" + SupportUtil.getAppVersionName(context)) httpCon.setRequestProperty("User-Agent", "Cowspent-android/" + SupportUtil.getAppVersionName(context))
if (lastETag != null && METHOD_GET == method) { if (lastETag != null && METHOD_GET == method) {
httpCon.setRequestProperty("If-None-Match", lastETag) httpCon.setRequestProperty("If-None-Match", lastETag)
} }
+6 -1
View File
@@ -35,6 +35,8 @@
<string name="title_settle">Settle Project</string> <string name="title_settle">Settle Project</string>
<string name="title_share">Share Project</string> <string name="title_share">Share Project</string>
<string name="title_add_project">Add Project</string> <string name="title_add_project">Add Project</string>
<string name="title_add_category">Add Category</string>
<string name="title_add_payment_mode">Add Payment Mode</string>
<string name="title_account">Nextcloud Account</string> <string name="title_account">Nextcloud Account</string>
<string name="title_share_web">Web link</string> <string name="title_share_web">Web link</string>
<string name="title_share_qr">Cowspent link</string> <string name="title_share_qr">Cowspent link</string>
@@ -133,6 +135,8 @@
<string name="settings_color_custom">Custom color</string> <string name="settings_color_custom">Custom color</string>
<string name="settings_color_mode">Color Selection</string> <string name="settings_color_mode">Color Selection</string>
<string name="settings_show_archived">Show archived projects</string> <string name="settings_show_archived">Show archived projects</string>
<string name="settings_beta_features">Beta Features</string>
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string> <string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
<string name="settings_colorpicker_title">Choose Color</string> <string name="settings_colorpicker_title">Choose Color</string>
@@ -153,6 +157,7 @@
<string name="pref_key_color_mode" translatable="false">colorMode</string> <string name="pref_key_color_mode" translatable="false">colorMode</string>
<string name="pref_key_offline_mode" translatable="false">offlineMode</string> <string name="pref_key_offline_mode" translatable="false">offlineMode</string>
<string name="pref_key_show_archived" translatable="false">showArchived</string> <string name="pref_key_show_archived" translatable="false">showArchived</string>
<string name="pref_key_beta_features" translatable="false">betaFeatures</string>
<string name="pref_value_night_mode_no" translatable="false">1</string> <string name="pref_value_night_mode_no" translatable="false">1</string>
<string name="pref_value_night_mode_yes" translatable="false">2</string> <string name="pref_value_night_mode_yes" translatable="false">2</string>
<string name="pref_value_night_mode_system" translatable="false">-1</string> <string name="pref_value_night_mode_system" translatable="false">-1</string>
@@ -229,7 +234,7 @@
<string name="settle_bill_what">Settlement</string> <string name="settle_bill_what">Settlement</string>
<!-- Currencies --> <!-- Currencies -->
<string name="currency_dialog_title">Choose Currency</string> <string name="currency_dialog_title">Choose Currency (%s)</string>
<string name="setting_none">None</string> <string name="setting_none">None</string>
<string name="setting_all">All</string> <string name="setting_all">All</string>
<string name="currency_saved_success">Currency settings saved.</string> <string name="currency_saved_success">Currency settings saved.</string>
-2
View File
@@ -19,8 +19,6 @@ This means you can choose where your data is going and preserve your privacy.
* Dark theme and customizable main app color * Dark theme and customizable main app color
* Share/import projects with link/QRCode * Share/import projects with link/QRCode
* Connect to a Nextcloud account to automatically add projects * Connect to a Nextcloud account to automatically add projects
* Background sync service with notifications on bills events
* Multi-lingual user-interface (translated on Crowdin: https://crowdin.com/project/cowspent)
# Requirements # Requirements
+3
View File
@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}