diff --git a/api/src/main/kotlin/de/chaosdorf/mete/BarcodeId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeId.kt similarity index 91% rename from api/src/main/kotlin/de/chaosdorf/mete/BarcodeId.kt rename to api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeId.kt index 61245654db6684cb8eaf6456a705f464cfe75d37..11454caf4ec4f1bf510f7f5bbd01081d21722863 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/BarcodeId.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeId.kt @@ -22,10 +22,12 @@ * THE SOFTWARE. */ -package de.chaosdorf.mete +package de.chaosdorf.mete.model import kotlinx.serialization.Serializable @Serializable @JvmInline -value class BarcodeId(val value: Long) +value class BarcodeId(val value: Long) { + override fun toString() = value.toString() +} diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeModel.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..12058586ce2cf565037e214d3c0220ddb9ac2fe5 --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeModel.kt @@ -0,0 +1,30 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete.model + +interface BarcodeModel { + val barcodeId: BarcodeId + val drinkId: DrinkId +} diff --git a/api/src/main/kotlin/de/chaosdorf/mete/DrinkId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkId.kt similarity index 91% rename from api/src/main/kotlin/de/chaosdorf/mete/DrinkId.kt rename to api/src/main/kotlin/de/chaosdorf/mete/model/DrinkId.kt index a7ce209dc7fb65fc629fa0ff8d3879134f861b90..4670a5f508c3481e33545108f39d1292bb12594a 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/DrinkId.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkId.kt @@ -22,10 +22,12 @@ * THE SOFTWARE. */ -package de.chaosdorf.mete +package de.chaosdorf.mete.model import kotlinx.serialization.Serializable @Serializable @JvmInline -value class DrinkId(val value: Long) +value class DrinkId(val value: Long) { + override fun toString() = value.toString() +} diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/AuditResponseV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkModel.kt similarity index 75% rename from api/src/main/kotlin/de/chaosdorf/mete/v1/AuditResponseV1.kt rename to api/src/main/kotlin/de/chaosdorf/mete/model/DrinkModel.kt index c0d79e1746447b91a38fe269eafb78472f6990bc..5019a19205bd5416866be4e26a76769886d1167c 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/v1/AuditResponseV1.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkModel.kt @@ -22,19 +22,16 @@ * THE SOFTWARE. */ -package de.chaosdorf.mete.v1 +package de.chaosdorf.mete.model -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable +import java.math.BigDecimal -@Serializable -data class AuditResponseV1( - @SerialName("payments_sum") - val payments: Double, - @SerialName("deposits_sum") - val deposits: Double, - @SerialName("sum") - val total: Double, - @SerialName("audits") - val entries: List<AuditEntryModelV1> -) +interface DrinkModel { + val drinkId: DrinkId + val name: String + val active: Boolean + val volume: BigDecimal + val caffeine: Int? + val price: BigDecimal + val logoUrl: String +} diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApi.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApi.kt new file mode 100644 index 0000000000000000000000000000000000000000..8313d89c82dceb7a519b58deeb4bd193be14c109 --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApi.kt @@ -0,0 +1,42 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete.model + +import java.math.BigDecimal + +interface MeteApi { + suspend fun getManifest(): PwaManifest? + suspend fun listTransactions(user: UserId? = null): TransactionSummaryModel + suspend fun listBarcodes(): List<BarcodeModel> + suspend fun getBarcode(id: BarcodeId): BarcodeModel? + suspend fun listDrinks(): List<DrinkModel> + suspend fun getDrink(id: DrinkId): DrinkModel? + suspend fun listUsers(): List<UserModel> + suspend fun getUser(id: UserId): UserModel? + suspend fun deposit(id: UserId, amount: BigDecimal) + suspend fun withdraw(id: UserId, amount: BigDecimal) + suspend fun purchase(id: UserId, drink: DrinkId) + suspend fun purchase(id: UserId, barcode: BarcodeId) +} diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiFactory.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApiFactory.kt similarity index 91% rename from api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiFactory.kt rename to api/src/main/kotlin/de/chaosdorf/mete/model/MeteApiFactory.kt index a120e4eeecd292d1a1e4ee8b843ca4c1b797049a..ee4da8b862b0aabf6838979c3d7939688d2127f6 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiFactory.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApiFactory.kt @@ -22,8 +22,8 @@ * THE SOFTWARE. */ -package de.chaosdorf.mete.v1 +package de.chaosdorf.mete.model -interface MeteApiFactory<T> { - fun newInstance(baseUrl: String): T +interface MeteApiFactory { + fun newInstance(baseUrl: String): MeteApi } diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/PurchaseSummaryModel.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/PurchaseSummaryModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..be21ea4013e53123d21c83eae39e5fde34c0e01b --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/PurchaseSummaryModel.kt @@ -0,0 +1,35 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete.model + +import java.math.BigDecimal + +interface TransactionSummaryModel { + val payments: BigDecimal + val deposits: BigDecimal + val total: BigDecimal + val entries: List<TransactionModel> +} + diff --git a/api/src/main/kotlin/de/chaosdorf/mete/PwaIcon.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/PwaIcon.kt similarity index 97% rename from api/src/main/kotlin/de/chaosdorf/mete/PwaIcon.kt rename to api/src/main/kotlin/de/chaosdorf/mete/model/PwaIcon.kt index 7734a53237cc1ac1025219e912cfc7047ef4cc47..ce833d8b6237d77972b0f1aec96c5c30bf0e0dd3 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/PwaIcon.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/PwaIcon.kt @@ -22,7 +22,7 @@ * THE SOFTWARE. */ -package de.chaosdorf.mete +package de.chaosdorf.mete.model import kotlinx.serialization.Serializable diff --git a/api/src/main/kotlin/de/chaosdorf/mete/PwaManifest.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/PwaManifest.kt similarity index 97% rename from api/src/main/kotlin/de/chaosdorf/mete/PwaManifest.kt rename to api/src/main/kotlin/de/chaosdorf/mete/model/PwaManifest.kt index 2732335e5c80baef808284b78648b854f3f1a789..a3fad8799b82b824a885891c8464596cb0637a13 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/PwaManifest.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/PwaManifest.kt @@ -22,7 +22,7 @@ * THE SOFTWARE. */ -package de.chaosdorf.mete +package de.chaosdorf.mete.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/api/src/main/kotlin/de/chaosdorf/mete/AuditEntryId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionId.kt similarity index 90% rename from api/src/main/kotlin/de/chaosdorf/mete/AuditEntryId.kt rename to api/src/main/kotlin/de/chaosdorf/mete/model/TransactionId.kt index d1725c4307a22b314cc66aeccdac97980edc0c69..8b3a85e9462b794b507b18efa18d6426fa8f79b9 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/AuditEntryId.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionId.kt @@ -22,10 +22,12 @@ * THE SOFTWARE. */ -package de.chaosdorf.mete +package de.chaosdorf.mete.model import kotlinx.serialization.Serializable @Serializable @JvmInline -value class AuditEntryId(val value: Long) +value class TransactionId(val value: Long) { + override fun toString() = value.toString() +} diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/AuditEntryModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionModel.kt similarity index 77% rename from api/src/main/kotlin/de/chaosdorf/mete/v1/AuditEntryModelV1.kt rename to api/src/main/kotlin/de/chaosdorf/mete/model/TransactionModel.kt index b3cf90126b925d431e3a6d6b5c38090482e6c1c3..43bd5cb78589444f7e14b24403242acb202fa9c5 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/v1/AuditEntryModelV1.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionModel.kt @@ -22,19 +22,14 @@ * THE SOFTWARE. */ -package de.chaosdorf.mete.v1 +package de.chaosdorf.mete.model -import de.chaosdorf.mete.AuditEntryId -import de.chaosdorf.mete.DrinkId import kotlinx.datetime.Instant -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable +import java.math.BigDecimal -@Serializable -data class AuditEntryModelV1( - val id: AuditEntryId, - @SerialName("created_at") - val createdAt: Instant, - val difference: Double, - val drink: DrinkId? -) +interface TransactionModel { + val transactionId: TransactionId + val difference: BigDecimal + val drinkId: DrinkId? + val timestamp: Instant +} diff --git a/api/src/main/kotlin/de/chaosdorf/mete/UserId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/UserId.kt similarity index 91% rename from api/src/main/kotlin/de/chaosdorf/mete/UserId.kt rename to api/src/main/kotlin/de/chaosdorf/mete/model/UserId.kt index 1f72b510a21243674af3e290c5aee0be9fef9541..0eaacf4cd8980a88c6345034e324d89468110f63 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/UserId.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/UserId.kt @@ -22,10 +22,12 @@ * THE SOFTWARE. */ -package de.chaosdorf.mete +package de.chaosdorf.mete.model import kotlinx.serialization.Serializable @Serializable @JvmInline -value class UserId(val value: Long) +value class UserId(val value: Long) { + override fun toString() = value.toString() +} diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/UserModel.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/UserModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..6d46048b5dce582564c39cf671f0470adbf8de16 --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/UserModel.kt @@ -0,0 +1,37 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete.model + +import java.math.BigDecimal + +interface UserModel { + val userId: UserId + val active: Boolean + val name: String + val email: String + val balance: BigDecimal + val audit: Boolean + val redirect: Boolean +} diff --git a/api/src/main/kotlin/de/chaosdorf/mete/util/BigDecimalSerializer.kt b/api/src/main/kotlin/de/chaosdorf/mete/util/BigDecimalSerializer.kt new file mode 100644 index 0000000000000000000000000000000000000000..4e1a4489be4b9429a18eb06bfb869378d90c92ca --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/util/BigDecimalSerializer.kt @@ -0,0 +1,44 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete.util + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.math.BigDecimal + +object BigDecimalSerializer : KSerializer<BigDecimal> { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("BigDecimal", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: BigDecimal) = + encoder.encodeString(value.toPlainString()) + + override fun deserialize(decoder: Decoder): BigDecimal = + BigDecimal(decoder.decodeString()) +} diff --git a/api/src/main/kotlin/de/chaosdorf/mete/util/Code204Interceptor.kt b/api/src/main/kotlin/de/chaosdorf/mete/util/Code204Interceptor.kt new file mode 100644 index 0000000000000000000000000000000000000000..a64e3726507f064ea134a7be0d0cb971344770e1 --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/util/Code204Interceptor.kt @@ -0,0 +1,41 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete.util + +import okhttp3.Interceptor +import okhttp3.Response + +object Code204Interceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val response = chain.proceed(request) + + return if (response.code == 204) { + response.newBuilder().code(200).build() + } else { + response + } + } +} diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/BarcodeModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/BarcodeModelV1.kt index 93f8461efe0f3e6e8e93d758ea22ed98cda0e4b3..ea35acb4197211452e87a17a4660bb5edfce8463 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/v1/BarcodeModelV1.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/BarcodeModelV1.kt @@ -24,12 +24,16 @@ package de.chaosdorf.mete.v1 -import de.chaosdorf.mete.BarcodeId -import de.chaosdorf.mete.DrinkId +import de.chaosdorf.mete.model.BarcodeId +import de.chaosdorf.mete.model.BarcodeModel +import de.chaosdorf.mete.model.DrinkId +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class BarcodeModelV1( - val id: BarcodeId, - val drink: DrinkId, -) +internal data class BarcodeModelV1( + @SerialName("id") + override val barcodeId: BarcodeId, + @SerialName("drink") + override val drinkId: DrinkId, +) : BarcodeModel diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt index bba984b3d95d0622a87dd7b238c5ab85b028db72..30755a7165cd70c49500a8ba095f09acd4d9025f 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/DrinkModelV1.kt @@ -24,19 +24,30 @@ package de.chaosdorf.mete.v1 -import de.chaosdorf.mete.DrinkId +import de.chaosdorf.mete.model.DrinkId +import de.chaosdorf.mete.model.DrinkModel +import de.chaosdorf.mete.util.BigDecimalSerializer import kotlinx.datetime.Instant import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import java.math.BigDecimal @Serializable -data class DrinkModelV1( - val id: DrinkId, - val name: String, +internal data class DrinkModelV1( + @SerialName("id") + override val drinkId: DrinkId, + @SerialName("name") + override val name: String, + @SerialName("active") + override val active: Boolean, @SerialName("bottle_size") - val bottleSize: Double, - val caffeine: Int?, - val price: Double, + @Serializable(with = BigDecimalSerializer::class) + override val volume: BigDecimal, + @SerialName("caffeine") + override val caffeine: Int?, + @SerialName("price") + @Serializable(with = BigDecimalSerializer::class) + override val price: BigDecimal, @SerialName("logo_file_name") val logoFileName: String, @SerialName("created_at") @@ -50,6 +61,5 @@ data class DrinkModelV1( @SerialName("logo_updated_at") val logoUpdatedAt: Instant, @SerialName("logo_url") - val logoUrl: String, - val active: Boolean -) + override val logoUrl: String, +) : DrinkModel diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt index d08da96ac03856aa36bc128445742e2f17e238cf..7c25ea6828c4a9ef0e2fd1a52192c315cca613ea 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt @@ -24,52 +24,71 @@ package de.chaosdorf.mete.v1 -import de.chaosdorf.mete.DrinkId -import de.chaosdorf.mete.PwaManifest -import de.chaosdorf.mete.UserId +import de.chaosdorf.mete.model.BarcodeId +import de.chaosdorf.mete.model.DrinkId +import de.chaosdorf.mete.model.MeteApi +import de.chaosdorf.mete.model.PwaManifest +import de.chaosdorf.mete.model.UserId +import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query +import java.math.BigDecimal -interface MeteApiV1 { +internal interface MeteApiV1 : MeteApi { @GET("manifest.json") - suspend fun getManifest(): PwaManifest? + override suspend fun getManifest(): PwaManifest? @GET("api/v1/audits.json") - suspend fun getAudits( - @Query("user") user: Long? = null - ): AuditResponseV1 + override suspend fun listTransactions( + @Query("user") user: UserId? + ): TransactionSummaryModelV1 @GET("api/v1/barcodes.json") - suspend fun listBarcodes(): List<BarcodeModelV1> + override suspend fun listBarcodes(): List<BarcodeModelV1> @GET("api/v1/barcodes/{id}.json") - suspend fun getBarcode(): BarcodeModelV1? + override suspend fun getBarcode( + @Path("id") id: BarcodeId + ): BarcodeModelV1? @GET("api/v1/drinks.json") - suspend fun listDrinks(): List<DrinkModelV1> + override suspend fun listDrinks(): List<DrinkModelV1> @GET("api/v1/drinks/{id}.json") - suspend fun getDrink(@Path("id") id: DrinkId): DrinkModelV1? + override suspend fun getDrink( + @Path("id") id: DrinkId + ): DrinkModelV1? @GET("api/v1/users.json") - suspend fun listUsers(): List<UserModelV1> + override suspend fun listUsers(): List<UserModelV1> @GET("api/v1/users/{id}.json") - suspend fun getUser(@Path("id") id: UserId): UserModelV1? + override suspend fun getUser( + @Path("id") id: UserId + ): UserModelV1? @GET("api/v1/users/{id}/deposit.json") - suspend fun deposit(@Path("id") id: UserId) + override suspend fun deposit( + @Path("id") id: UserId, + @Query("amount") amount: BigDecimal + ) @GET("api/v1/users/{id}/payment.json") - suspend fun payment(@Path("id") id: UserId) + override suspend fun withdraw( + @Path("id") id: UserId, + @Query("amount") amount: BigDecimal + ) @GET("api/v1/users/{id}/buy.json") - suspend fun buy(@Path("id") id: UserId) + override suspend fun purchase( + @Path("id") id: UserId, + @Query("drink") drink: DrinkId + ) @GET("api/v1/users/{id}/buy_barcode.json") - suspend fun buyWithBarcode(@Path("id") id: UserId) - - @GET("api/v1/users/stats.json") - suspend fun getStats() + override suspend fun purchase( + @Path("id") id: UserId, + @Query("barcode") barcode: BarcodeId + ) } diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1Factory.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1Factory.kt index 2e51fec5877d10c5a8885ea4b1103471d25d1e55..3fdc434304fe940d8d8c6b1223df7c0e474e314a 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1Factory.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1Factory.kt @@ -25,19 +25,28 @@ package de.chaosdorf.mete.v1 import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import de.chaosdorf.mete.model.MeteApi +import de.chaosdorf.mete.model.MeteApiFactory +import de.chaosdorf.mete.util.Code204Interceptor import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.create -object MeteApiV1Factory : MeteApiFactory<MeteApiV1> { +object MeteApiV1Factory : MeteApiFactory { private val json = Json { ignoreUnknownKeys = true } - override fun newInstance(baseUrl: String): MeteApiV1 { + private val httpClient = OkHttpClient.Builder() + .addInterceptor(Code204Interceptor) + .build() + + override fun newInstance(baseUrl: String): MeteApi { val contentType = "application/json".toMediaType() val retrofit = Retrofit.Builder() + .client(httpClient) .baseUrl(baseUrl) .addConverterFactory(json.asConverterFactory(contentType)) .build() diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionModelV1.kt new file mode 100644 index 0000000000000000000000000000000000000000..02a1b7b565384889704252add85f7f16126447bf --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionModelV1.kt @@ -0,0 +1,47 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete.v1 + +import de.chaosdorf.mete.model.DrinkId +import de.chaosdorf.mete.model.TransactionId +import de.chaosdorf.mete.model.TransactionModel +import de.chaosdorf.mete.util.BigDecimalSerializer +import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.math.BigDecimal + +@Serializable +internal data class TransactionModelV1( + @SerialName("id") + override val transactionId: TransactionId, + @SerialName("difference") + @Serializable(with = BigDecimalSerializer::class) + override val difference: BigDecimal, + @SerialName("drink") + override val drinkId: DrinkId?, + @SerialName("created_at") + override val timestamp: Instant +) : TransactionModel diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionSummaryModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionSummaryModelV1.kt new file mode 100644 index 0000000000000000000000000000000000000000..7edd2343b9c96fe7fc4585a2634d8448244289c5 --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/TransactionSummaryModelV1.kt @@ -0,0 +1,46 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete.v1 + +import de.chaosdorf.mete.model.TransactionSummaryModel +import de.chaosdorf.mete.util.BigDecimalSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.math.BigDecimal + +@Serializable +internal data class TransactionSummaryModelV1( + @SerialName("payments_sum") + @Serializable(with = BigDecimalSerializer::class) + override val payments: BigDecimal, + @SerialName("deposits_sum") + @Serializable(with = BigDecimalSerializer::class) + override val deposits: BigDecimal, + @SerialName("sum") + @Serializable(with = BigDecimalSerializer::class) + override val total: BigDecimal, + @SerialName("audits") + override val entries: List<TransactionModelV1> +) : TransactionSummaryModel diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt index 33c76ca752c587270267ca5f6cca01f5e1e35ef1..75d6edccca00a13fd028679787d40cfcd4bdb27d 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/UserModelV1.kt @@ -24,22 +24,33 @@ package de.chaosdorf.mete.v1 -import de.chaosdorf.mete.UserId +import de.chaosdorf.mete.model.UserId +import de.chaosdorf.mete.model.UserModel +import de.chaosdorf.mete.util.BigDecimalSerializer import kotlinx.datetime.Instant import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import java.math.BigDecimal @Serializable -data class UserModelV1( - val id: UserId, - val name: String, - val email: String, +internal data class UserModelV1( + @SerialName("id") + override val userId: UserId, + @SerialName("name") + override val name: String, + @SerialName("email") + override val email: String, + @SerialName("balance") + @Serializable(with = BigDecimalSerializer::class) + override val balance: BigDecimal, + @SerialName("active") + override val active: Boolean, + @SerialName("audit") + override val audit: Boolean, + @SerialName("redirect") + override val redirect: Boolean, @SerialName("created_at") val createdAt: Instant, @SerialName("updated_at") val updatedAt: Instant, - val balance: Double, - val active: Boolean, - val audit: Boolean, - val redirect: Boolean -) +) : UserModel diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e68570b35f3d3f16626d1d7b054277297ab2eba8..64e87db574fa38013b822565221a91c122f718f0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -38,6 +38,7 @@ android { getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" ) + signingConfig = signingConfigs.getByName("debug") } getByName("debug") { diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt index 8d35e31128fafdc943f6f8be158020b3f0522f2c..c531e4c8ef1ef2fc291038aab8ba21e8c54ee72c 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt @@ -31,10 +31,12 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import de.chaosdorf.mete.model.MeteApiFactory +import de.chaosdorf.mete.v1.MeteApiV1Factory import de.chaosdorf.meteroid.MeteroidDatabase import de.chaosdorf.meteroid.model.DrinkRepository -import de.chaosdorf.meteroid.model.PurchaseRepository import de.chaosdorf.meteroid.model.ServerRepository +import de.chaosdorf.meteroid.model.TransactionRepository import de.chaosdorf.meteroid.model.UserRepository import javax.inject.Singleton @@ -61,12 +63,15 @@ object DatabaseModule { ): UserRepository = database.users() @Provides - fun providePurchaseRepository( + fun provideTransactionRepository( database: MeteroidDatabase - ): PurchaseRepository = database.purchases() + ): TransactionRepository = database.transactions() @Provides fun provideServerRepository( database: MeteroidDatabase ): ServerRepository = database.server() + + @Provides + fun providesMeteFactory(): MeteApiFactory = MeteApiV1Factory } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sample/SampleDrinkProvider.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sample/SampleDrinkProvider.kt index c29e736c162cbb4082ca818151c0415667082468..690b73c4ae6e870f337944de0061df471391c58c 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sample/SampleDrinkProvider.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sample/SampleDrinkProvider.kt @@ -25,44 +25,31 @@ package de.chaosdorf.meteroid.sample import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import de.chaosdorf.mete.DrinkId +import de.chaosdorf.mete.model.DrinkId import de.chaosdorf.meteroid.model.Drink import de.chaosdorf.meteroid.model.ServerId -import kotlinx.datetime.Instant class SampleDrinkProvider : PreviewParameterProvider<Drink> { override val values = sequenceOf( - Drink( - serverId = ServerId(-1), - drinkId = DrinkId(27), - active = true, - name = "Club Mate", - volume = 0.5, - caffeine = null, - price = 1.5, - createdAt = Instant.fromEpochMilliseconds(1684598011800), - updatedAt = Instant.fromEpochMilliseconds(1684607122132), - logoUrl = "http://192.168.188.36:8080/system/drinks/logos/000/000/027/thumb/logo.png", - logoFileName = "logo.png", - logoContentType = "image/png", - logoFileSize = 183063, - logoUpdatedAt = Instant.fromEpochMilliseconds(1684607121995) - ), - Drink( - serverId = ServerId(-1), - drinkId = DrinkId(15), - active = false, - name = "Paulaner Spezi", - volume = 0.5, - caffeine = null, - price = 1.5, - createdAt = Instant.fromEpochMilliseconds(1684597806099), - updatedAt = Instant.fromEpochMilliseconds(1684607346944), - logoUrl = "http://192.168.188.36:8080/system/drinks/logos/000/000/015/thumb/logo.png", - logoFileName = "logo.png", - logoContentType = "image/png", - logoFileSize = 173265, - logoUpdatedAt = Instant.fromEpochMilliseconds(1684607346835) - ) + Drink( + serverId = ServerId(-1), + drinkId = DrinkId(27), + active = true, + name = "Club Mate", + volume = 0.5.toBigDecimal(), + caffeine = null, + price = 1.5.toBigDecimal(), + logoUrl = "http://192.168.188.36:8080/system/drinks/logos/000/000/027/thumb/logo.png", + ), + Drink( + serverId = ServerId(-1), + drinkId = DrinkId(15), + active = false, + name = "Paulaner Spezi", + volume = 0.5.toBigDecimal(), + caffeine = null, + price = 1.5.toBigDecimal(), + logoUrl = "http://192.168.188.36:8080/system/drinks/logos/000/000/015/thumb/logo.png", + ) ) } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt index 75fdfb2c0b0a3711011d360533e1fdd3f41baf39..3d0751f4e636528100cd2f74526e2323f598df13 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt @@ -24,7 +24,7 @@ package de.chaosdorf.meteroid.storage -import de.chaosdorf.mete.UserId +import de.chaosdorf.mete.model.UserId import de.chaosdorf.meteroid.model.ServerId import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt index 71a6be1d7ecb1b9215e474ab6bcbf1590a40da3e..62f4b809c7afbcad2bbd2a8ff02dd1875660a234 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt @@ -28,7 +28,7 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.longPreferencesKey -import de.chaosdorf.mete.UserId +import de.chaosdorf.mete.model.UserId import de.chaosdorf.meteroid.model.ServerId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.mapLatest diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt index dc8e5ed99634ea494b8f9269b6255cb4bd049732..d9d2a162ff3c962aa0eac128b059985af0e1460d 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt @@ -25,8 +25,8 @@ package de.chaosdorf.meteroid.sync import androidx.room.withTransaction -import de.chaosdorf.mete.DrinkId -import de.chaosdorf.mete.v1.MeteApiV1Factory +import de.chaosdorf.mete.model.DrinkId +import de.chaosdorf.mete.model.MeteApiFactory import de.chaosdorf.meteroid.MeteroidDatabase import de.chaosdorf.meteroid.model.Drink import de.chaosdorf.meteroid.model.DrinkRepository @@ -35,6 +35,7 @@ import de.chaosdorf.meteroid.model.ServerId import javax.inject.Inject class DrinkSyncHandler @Inject constructor( + private val factory: MeteApiFactory, private val db: MeteroidDatabase, private val repository: DrinkRepository ) : SyncHandler<Server, Drink, DrinkSyncHandler.Key>() { @@ -57,8 +58,8 @@ class DrinkSyncHandler @Inject constructor( repository.getAll(context.serverId) override suspend fun loadCurrent(context: Server): List<Drink> { - val api = MeteApiV1Factory.newInstance(context.url) + val api = factory.newInstance(context.url) val loadedEntries = api.listDrinks() - return loadedEntries.map { Drink.fromModelV1(context, it) } + return loadedEntries.map { Drink.fromModel(context, it) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/PurchaseSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/PurchaseSyncHandler.kt index 8ba6652d07b5d7958abab18f18b3813d4ce9fe2b..071c20c4881f368c6397e03c370e69cc1a6f6168 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/PurchaseSyncHandler.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/PurchaseSyncHandler.kt @@ -25,42 +25,43 @@ package de.chaosdorf.meteroid.sync import androidx.room.withTransaction -import de.chaosdorf.mete.AuditEntryId -import de.chaosdorf.mete.UserId -import de.chaosdorf.mete.v1.MeteApiV1Factory +import de.chaosdorf.mete.model.MeteApiFactory +import de.chaosdorf.mete.model.TransactionId +import de.chaosdorf.mete.model.UserId import de.chaosdorf.meteroid.MeteroidDatabase -import de.chaosdorf.meteroid.model.Purchase -import de.chaosdorf.meteroid.model.PurchaseRepository import de.chaosdorf.meteroid.model.Server import de.chaosdorf.meteroid.model.ServerId +import de.chaosdorf.meteroid.model.Transaction +import de.chaosdorf.meteroid.model.TransactionRepository import javax.inject.Inject -class PurchaseSyncHandler @Inject constructor( +class TransactionSyncHandler @Inject constructor( + private val factory: MeteApiFactory, private val db: MeteroidDatabase, - private val repository: PurchaseRepository -) : SyncHandler<Pair<Server, UserId>, Purchase, PurchaseSyncHandler.Key>() { + private val repository: TransactionRepository +) : SyncHandler<Pair<Server, UserId>, Transaction, TransactionSyncHandler.Key>() { data class Key( - val server: ServerId, val purchase: AuditEntryId + val server: ServerId, val transaction: TransactionId ) override suspend fun withTransaction(block: suspend () -> Unit) = db.withTransaction(block) - override suspend fun store(entry: Purchase) = + override suspend fun store(entry: Transaction) = repository.save(entry) override suspend fun delete(key: Key) = - repository.delete(key.server, key.purchase) + repository.delete(key.server, key.transaction) - override fun entryToKey(entry: Purchase) = Key(entry.serverId, entry.purchaseId) + override fun entryToKey(entry: Transaction) = Key(entry.serverId, entry.transactionId) - override suspend fun loadStored(context: Pair<Server, UserId>): List<Purchase> = + override suspend fun loadStored(context: Pair<Server, UserId>): List<Transaction> = repository.getAll(context.first.serverId, context.second) - override suspend fun loadCurrent(context: Pair<Server, UserId>): List<Purchase> { + override suspend fun loadCurrent(context: Pair<Server, UserId>): List<Transaction> { val (server, userId) = context - val api = MeteApiV1Factory.newInstance(server.url) - val loadedEntries = api.getAudits(user = userId.value).entries - return loadedEntries.map { Purchase.fromModelV1(server, userId, it) } + val api = factory.newInstance(server.url) + val loadedEntries = api.listTransactions(user = userId).entries + return loadedEntries.map { Transaction.fromModel(server, userId, it) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt index a6f9291d3db387c6737d8f3f1ba64ca03d320124..179a5dbd20dd46c856f3ad6e99447bc1ceffad4b 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt @@ -25,8 +25,8 @@ package de.chaosdorf.meteroid.sync import androidx.room.withTransaction -import de.chaosdorf.mete.UserId -import de.chaosdorf.mete.v1.MeteApiV1Factory +import de.chaosdorf.mete.model.MeteApiFactory +import de.chaosdorf.mete.model.UserId import de.chaosdorf.meteroid.MeteroidDatabase import de.chaosdorf.meteroid.model.Server import de.chaosdorf.meteroid.model.ServerId @@ -35,6 +35,7 @@ import de.chaosdorf.meteroid.model.UserRepository import javax.inject.Inject class UserSyncHandler @Inject constructor( + private val factory: MeteApiFactory, private val db: MeteroidDatabase, private val repository: UserRepository ) : SyncHandler<Server, User, UserSyncHandler.Key>() { @@ -57,8 +58,8 @@ class UserSyncHandler @Inject constructor( repository.getAll(context.serverId) override suspend fun loadCurrent(context: Server): List<User> { - val api = MeteApiV1Factory.newInstance(context.url) + val api = factory.newInstance(context.url) val loadedEntries = api.listUsers() - return loadedEntries.map { User.fromModelV1(context, it) } + return loadedEntries.map { User.fromModel(context, it) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt index 81b5e89523a17d84e13df25d394e7c4cc2650732..6b2e6540131ebd72501e8060defaa00c878ef57b 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt @@ -26,6 +26,7 @@ package de.chaosdorf.meteroid.ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -34,21 +35,23 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavOptions import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController +import de.chaosdorf.meteroid.ui.Transactions.TransactionListScreen import de.chaosdorf.meteroid.ui.drinks.DrinkListScreen import de.chaosdorf.meteroid.ui.drinks.DrinkListViewModel import de.chaosdorf.meteroid.ui.money.MoneyListScreen import de.chaosdorf.meteroid.ui.money.MoneyListViewModel import de.chaosdorf.meteroid.ui.navigation.Routes -import de.chaosdorf.meteroid.ui.purchases.PurchaseListScreen -import de.chaosdorf.meteroid.ui.purchases.PurchaseViewModel import de.chaosdorf.meteroid.ui.servers.AddServerScreen import de.chaosdorf.meteroid.ui.servers.ServerListScreen +import de.chaosdorf.meteroid.ui.transactions.TransactionViewModel import de.chaosdorf.meteroid.ui.users.UserListScreen import kotlinx.coroutines.launch @@ -60,18 +63,37 @@ fun AppRouter(viewModel: AppViewModel = viewModel()) { LaunchedEffect(initState) { when (initState) { - AppViewModel.InitState.LOADING -> navController.navigate(Routes.Init) - AppViewModel.InitState.CREATE_SERVER -> navController.navigate(Routes.Servers.Add) - AppViewModel.InitState.SELECT_SERVER -> navController.navigate(Routes.Servers.List) - AppViewModel.InitState.SELECT_USER -> navController.navigate(Routes.Users.List) - AppViewModel.InitState.HOME -> navController.navigate(Routes.Home.Root) + AppViewModel.InitState.LOADING -> navController.navigate( + Routes.Init, + NavOptions.Builder().setPopUpTo(Routes.Init, true).build() + ) + + AppViewModel.InitState.CREATE_SERVER -> navController.navigate( + Routes.Servers.Add, + NavOptions.Builder().setPopUpTo(Routes.Servers.Add, true).build() + ) + + AppViewModel.InitState.SELECT_SERVER -> navController.navigate( + Routes.Servers.List, + NavOptions.Builder().setPopUpTo(Routes.Servers.List, true).build() + ) + + AppViewModel.InitState.SELECT_USER -> navController.navigate( + Routes.Users.List, + NavOptions.Builder().setPopUpTo(Routes.Users.List, true).build() + ) + + AppViewModel.InitState.HOME -> navController.navigate( + Routes.Home.Root, + NavOptions.Builder().setPopUpTo(Routes.Home.Root, true).build() + ) } } NavHost(navController, startDestination = Routes.Init) { composable(route = Routes.Init) { _ -> - Box(contentAlignment = Alignment.Center) { - Column { + Box(Modifier.fillMaxSize()) { + Column(Modifier.align(Alignment.Center)) { CircularProgressIndicator() Text("Loading") } @@ -131,8 +153,8 @@ fun AppRouter(viewModel: AppViewModel = viewModel()) { MoneyListScreen(moneyListViewModel, navController::navigate) } composable(Routes.Home.History) { _ -> - val purchaseViewModel = hiltViewModel<PurchaseViewModel>() - PurchaseListScreen(purchaseViewModel, navController::navigate) + val transactionViewModel = hiltViewModel<TransactionViewModel>() + TransactionListScreen(transactionViewModel, navController::navigate) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt index bd066bb443087ce79a3443a72e69524010a02fc5..751902f50c52a6588f40ba9236367a68a61bebd5 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt @@ -27,14 +27,14 @@ package de.chaosdorf.meteroid.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import de.chaosdorf.mete.UserId +import de.chaosdorf.mete.model.UserId import de.chaosdorf.meteroid.model.Server import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.model.ServerRepository import de.chaosdorf.meteroid.storage.AccountPreferences import de.chaosdorf.meteroid.sync.AccountProvider import de.chaosdorf.meteroid.sync.DrinkSyncHandler -import de.chaosdorf.meteroid.sync.PurchaseSyncHandler +import de.chaosdorf.meteroid.sync.TransactionSyncHandler import de.chaosdorf.meteroid.sync.UserSyncHandler import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -51,9 +51,7 @@ class AppViewModel @Inject constructor( accountProvider: AccountProvider, private val accountPreferences: AccountPreferences, private val serverRepository: ServerRepository, - private val userSyncHandler: UserSyncHandler, - private val drinkSyncHandler: DrinkSyncHandler, - private val purchaseSyncHandler: PurchaseSyncHandler + private val syncManager: SyncManager ) : ViewModel() { val initState: StateFlow<InitState> = accountPreferences.state .flatMapLatest { preferences -> @@ -69,12 +67,8 @@ class AppViewModel @Inject constructor( init { accountProvider.account.distinctUntilChanged().onEach { account -> - account?.let { (server, user) -> - userSyncHandler.sync(server) - drinkSyncHandler.sync(server) - user?.let { user -> - purchaseSyncHandler.sync(Pair(server, user.userId)) - } + account?.let { (server, maybeUser) -> + syncManager.sync(server, maybeUser) } }.launchIn(viewModelScope) } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PriceBadge.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PriceBadge.kt new file mode 100644 index 0000000000000000000000000000000000000000..7ded488064f050a2dae6b47c24ceb08ba5ee660d --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PriceBadge.kt @@ -0,0 +1,62 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.ui + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Badge +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import java.math.BigDecimal + +@Composable +fun PriceBadge( + price: BigDecimal, + modifier: Modifier = Modifier, + containerColor: Color = + if (price > BigDecimal.ZERO) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.error, + textColor: Color = + if (price > BigDecimal.ZERO) MaterialTheme.colorScheme.onPrimary + else MaterialTheme.colorScheme.onError, + textStyle: TextStyle = MaterialTheme.typography.labelLarge +) { + Badge( + containerColor = containerColor, + modifier = modifier + ) { + Text( + "%.02f €".format(price), + style = textStyle, + color = textColor, + modifier = Modifier.padding(8.dp, 4.dp) + ) + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SyncManager.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SyncManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..48276de223b25a2930a5d95c7a7185162f1e249d --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SyncManager.kt @@ -0,0 +1,75 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.ui + +import de.chaosdorf.mete.model.MeteApiFactory +import de.chaosdorf.meteroid.model.AccountInfo +import de.chaosdorf.meteroid.model.Drink +import de.chaosdorf.meteroid.model.Server +import de.chaosdorf.meteroid.model.User +import de.chaosdorf.meteroid.sync.DrinkSyncHandler +import de.chaosdorf.meteroid.sync.TransactionSyncHandler +import de.chaosdorf.meteroid.sync.UserSyncHandler +import java.math.BigDecimal +import javax.inject.Inject + +class SyncManager @Inject constructor( + private val factory: MeteApiFactory, + private val userSyncHandler: UserSyncHandler, + private val drinkSyncHandler: DrinkSyncHandler, + private val transactionSyncHandler: TransactionSyncHandler +) { + suspend fun sync(server: Server, user: User?) { + userSyncHandler.sync(server) + drinkSyncHandler.sync(server) + if (user != null) { + transactionSyncHandler.sync(Pair(server, user.userId)) + } + } + + suspend fun purchase(account: AccountInfo, drink: Drink) { + account.user?.let { user -> + val api = factory.newInstance(account.server.url) + api.purchase(user.userId, drink.drinkId) + sync(account.server, user) + } + } + + suspend fun deposit(account: AccountInfo, amount: BigDecimal) { + account.user?.let { user -> + val api = factory.newInstance(account.server.url) + api.deposit(user.userId, amount) + sync(account.server, user) + } + } + + suspend fun withdraw(account: AccountInfo, amount: BigDecimal) { + account.user?.let { user -> + val api = factory.newInstance(account.server.url) + api.withdraw(user.userId, amount) + sync(account.server, user) + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt new file mode 100644 index 0000000000000000000000000000000000000000..cdd66c369f44bffec4c57c3310ee314a4dc90064 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt @@ -0,0 +1,60 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.ui.drinks + +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.FilterChip +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun DrinkListFilterChip( + label: String, + selected: Boolean, + onClick: () -> Unit, +) { + FilterChip( + label = { + Text(label, style = MaterialTheme.typography.labelLarge) + }, + selected = selected, + leadingIcon = { + if (selected) { + Icon( + Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + }, + onClick = onClick + ) +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt index e0708c350d6b1158fa5404b9eea92a9aa111d82a..b872aef201dbb3264ce068cb1b82c92d86c360ac 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt @@ -26,12 +26,13 @@ package de.chaosdorf.meteroid.ui.drinks import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -39,11 +40,11 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.navigation.NavOptions -import de.chaosdorf.meteroid.sync.SyncHandler import de.chaosdorf.meteroid.ui.navigation.HomeSections import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar +@OptIn(ExperimentalLayoutApi::class) @Composable fun DrinkListScreen( viewModel: DrinkListViewModel, @@ -51,7 +52,8 @@ fun DrinkListScreen( ) { val account by viewModel.account.collectAsState() val drinks by viewModel.drinks.collectAsState() - val syncState by viewModel.syncState.collectAsState() + val filters by viewModel.filters.collectAsState() + Scaffold( topBar = { MeteroidTopBar(account, onNavigate) }, bottomBar = { @@ -62,18 +64,28 @@ fun DrinkListScreen( ) } ) { paddingValues: PaddingValues -> - Column { - if (syncState == SyncHandler.State.Loading) { - LinearProgressIndicator() + Column(Modifier.padding(paddingValues)) { + FlowRow( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + DrinkListFilterChip( + label = "Active", + selected = filters.contains(DrinkListViewModel.Filter.Active), + onClick = { viewModel.toggleFilter(DrinkListViewModel.Filter.Active) } + ) + DrinkListFilterChip( + label = "Coffeine Free", + selected = filters.contains(DrinkListViewModel.Filter.CaffeineFree), + onClick = { viewModel.toggleFilter(DrinkListViewModel.Filter.CaffeineFree) } + ) } LazyVerticalGrid( - GridCells.Adaptive(120.dp), - modifier = Modifier.padding(paddingValues), - contentPadding = PaddingValues(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, + GridCells.Adaptive(104.dp), + modifier = Modifier.padding(horizontal = 8.dp) ) { items(drinks) { drink -> - DrinkTile(drink) + DrinkTile(drink, viewModel::purchase) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt index bca3d100d6dd6521c646a588775cc897b6a134d0..045d3757633f910dcec4f91aa6ae76b29371e698 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt @@ -24,39 +24,73 @@ package de.chaosdorf.meteroid.ui.drinks +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import de.chaosdorf.meteroid.model.AccountInfo import de.chaosdorf.meteroid.model.Drink import de.chaosdorf.meteroid.model.DrinkRepository -import de.chaosdorf.meteroid.model.Server -import de.chaosdorf.meteroid.model.User import de.chaosdorf.meteroid.sync.AccountProvider -import de.chaosdorf.meteroid.sync.DrinkSyncHandler -import de.chaosdorf.meteroid.sync.SyncHandler +import de.chaosdorf.meteroid.ui.SyncManager +import de.chaosdorf.meteroid.util.update import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class DrinkListViewModel @Inject constructor( accountProvider: AccountProvider, repository: DrinkRepository, - syncHandler: DrinkSyncHandler + private val syncManager: SyncManager, + private val savedStateHandle: SavedStateHandle ) : ViewModel() { val account: StateFlow<AccountInfo?> = accountProvider.account .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) - val drinks: StateFlow<List<Drink>> = accountProvider.account - .flatMapLatest { account -> + val filters: StateFlow<Set<Filter>> = + savedStateHandle.getStateFlow("filters", setOf(Filter.Active)) + + val drinks: StateFlow<List<Drink>> = combine( + accountProvider.account.flatMapLatest { account -> account?.let { (server, _) -> repository.getAllFlow(server.serverId) } ?: flowOf(emptyList()) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + }, + filters + ) { drinks, filters -> + drinks.filter { item -> + filters.all { filter -> filter.matches(item) } + } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + + fun toggleFilter(filter: Filter) { + savedStateHandle.update<Set<Filter>>("filters", emptySet()) { filters -> + if (filters.contains(filter)) filters - filter + else filters + filter + } + } + + fun purchase(item: Drink) { + account.value?.let { account -> + viewModelScope.launch { + syncManager.purchase(account, item) + } + } + } + + enum class Filter { + CaffeineFree, + Active; - val syncState: StateFlow<SyncHandler.State> = syncHandler.state + fun matches(item: Drink) = when (this) { + CaffeineFree -> item.caffeine == 0 + Active -> item.active + } + } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt index 2099f684419e787c81d533635ac1f1d1f5c170af..7c141b9503ddef01638d7deeedb333a2707852e5 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt @@ -26,13 +26,18 @@ package de.chaosdorf.meteroid.ui.drinks import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.paddingFromBaseline import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme @@ -40,21 +45,24 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import coil.compose.rememberAsyncImagePainter import de.chaosdorf.meteroid.model.Drink import de.chaosdorf.meteroid.sample.SampleDrinkProvider +import de.chaosdorf.meteroid.ui.PriceBadge @Preview(widthDp = 120, showBackground = true) @Composable fun DrinkTile( - @PreviewParameter(SampleDrinkProvider::class) item: Drink + @PreviewParameter(SampleDrinkProvider::class) item: Drink, + onPurchase: (Drink) -> Unit = {} ) { val thumbPainter = rememberAsyncImagePainter( item.logoUrl @@ -65,7 +73,12 @@ fun DrinkTile( ) Column( - modifier = Modifier.padding(4.dp) + modifier = Modifier + .height(IntrinsicSize.Max) + .alpha(if (item.active) 1.0f else 0.67f) + .clip(RoundedCornerShape(8.dp)) + .clickable { onPurchase(item) } + .padding(8.dp) ) { Box { Image( @@ -73,39 +86,38 @@ fun DrinkTile( contentDescription = null, contentScale = ContentScale.Fit, modifier = Modifier + .aspectRatio(1.0f) .clip(CircleShape) .background(MaterialTheme.colorScheme.primaryContainer) - .aspectRatio(1.0f) - .padding(8.dp) ) - Text( - String.format("%.02f €", item.price), - color = MaterialTheme.colorScheme.onPrimary, - fontWeight = FontWeight.SemiBold, + PriceBadge( + item.price, modifier = Modifier - .padding(vertical = 12.dp) - .clip(RoundedCornerShape(16.dp)) - .background(MaterialTheme.colorScheme.primary) .align(Alignment.BottomEnd) - .padding(horizontal = 8.dp) + .paddingFromBaseline(bottom = 12.dp) ) } + Spacer(Modifier.height(4.dp)) Text( item.name, - fontWeight = FontWeight.SemiBold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(horizontal = 4.dp) - ) - Row( - horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier - .padding(horizontal = 4.dp) .fillMaxWidth() - ) { + .padding(horizontal = 8.dp), + textAlign = TextAlign.Center, + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.labelLarge, + ) + Spacer(Modifier.height(4.dp)) + Row(modifier = Modifier.align(Alignment.CenterHorizontally)) { Text( - String.format("%.02f l", item.volume), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) + String.format("%.02fl · %.02f€/l", item.volume, item.price / item.volume), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + textAlign = TextAlign.Center, + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) ) } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt index 14386cef5430a9d6a8393a024a497042d40828a0..78b8d667b6ae4abe6dc5577ea427d696e9eec2fb 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt @@ -67,7 +67,7 @@ fun MoneyListScreen( horizontalArrangement = Arrangement.SpaceBetween, ) { items(viewModel.money) { monetaryAmount -> - MoneyTile(monetaryAmount) + MoneyTile(monetaryAmount, viewModel::deposit) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt index 508ccec5a62e2b86e3f6e0b5799a5a690e755ce8..7ba2e82985c2c5dd2dbf348d2263fcca492ca4c5 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt @@ -28,30 +28,43 @@ import androidx.annotation.DrawableRes import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import de.chaosdorf.mete.model.MeteApiFactory import de.chaosdorf.meteroid.R import de.chaosdorf.meteroid.model.AccountInfo import de.chaosdorf.meteroid.sync.AccountProvider +import de.chaosdorf.meteroid.ui.SyncManager import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import java.math.BigDecimal import javax.inject.Inject -enum class MonetaryAmount(val amount: Double, @DrawableRes val image: Int) { - MONEY_50(0.50, R.drawable.euro_50), - MONEY_100(1.00, R.drawable.euro_100), - MONEY_200(2.00, R.drawable.euro_200), - MONEY_500(5.00, R.drawable.euro_500), - MONEY_1000(10.00, R.drawable.euro_1000), - MONEY_2000(20.00, R.drawable.euro_2000), - MONEY_5000(50.00, R.drawable.euro_5000), +enum class MonetaryAmount(val amount: BigDecimal, @DrawableRes val image: Int) { + MONEY_50(0.50.toBigDecimal(), R.drawable.euro_50), + MONEY_100(1.00.toBigDecimal(), R.drawable.euro_100), + MONEY_200(2.00.toBigDecimal(), R.drawable.euro_200), + MONEY_500(5.00.toBigDecimal(), R.drawable.euro_500), + MONEY_1000(10.00.toBigDecimal(), R.drawable.euro_1000), + MONEY_2000(20.00.toBigDecimal(), R.drawable.euro_2000), + MONEY_5000(50.00.toBigDecimal(), R.drawable.euro_5000), } @HiltViewModel class MoneyListViewModel @Inject constructor( - accountProvider: AccountProvider + accountProvider: AccountProvider, + private val syncManager: SyncManager ) : ViewModel() { val account: StateFlow<AccountInfo?> = accountProvider.account .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) val money: List<MonetaryAmount> = MonetaryAmount.entries + + fun deposit(item: MonetaryAmount) { + account.value?.let { account -> + viewModelScope.launch { + syncManager.deposit(account, item.amount) + } + } + } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt index 5ec0760bd633730e1cd07cba42ab38933b5e4d49..5254c6f925c62aa400fce52cba059e773f5f927b 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt @@ -26,50 +26,47 @@ package de.chaosdorf.meteroid.ui.money import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.paddingFromBaseline import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import de.chaosdorf.meteroid.ui.PriceBadge @Composable fun MoneyTile( - item: MonetaryAmount + item: MonetaryAmount, + onDeposit: (MonetaryAmount) -> Unit = {} ) { - Column( - modifier = Modifier.padding(4.dp) + Box( + modifier = Modifier + .height(IntrinsicSize.Max) + .clip(RoundedCornerShape(8.dp)) + .clickable { onDeposit(item) } ) { - Box { - Image( - painterResource(item.image), - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier - .aspectRatio(1.0f) - .padding(8.dp) - ) - Text( - String.format("%.02f €", item.amount), - color = MaterialTheme.colorScheme.onPrimary, - fontWeight = FontWeight.SemiBold, - modifier = Modifier - .padding(vertical = 12.dp) - .clip(RoundedCornerShape(16.dp)) - .background(MaterialTheme.colorScheme.primary) - .align(Alignment.BottomEnd) - .padding(horizontal = 8.dp) - ) - } + Image( + painterResource(item.image), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier + .aspectRatio(1.0f) + ) + PriceBadge( + item.amount, + modifier = Modifier + .align(Alignment.BottomEnd) + .paddingFromBaseline(bottom = 24.dp) + ) } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt index e796d4f902d46000b4572c7cded48531ec5452f3..3a5d95e85a3da122ca99cf6b4287be9c20cd99ba 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt @@ -25,93 +25,94 @@ package de.chaosdorf.meteroid.ui.navigation import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarColors -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavOptions import coil.compose.AsyncImage import de.chaosdorf.meteroid.model.AccountInfo -import de.chaosdorf.meteroid.model.Server -import de.chaosdorf.meteroid.model.User +import de.chaosdorf.meteroid.ui.PriceBadge +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.internal.toCanonicalHost @Composable fun MeteroidTopBar( account: AccountInfo?, onNavigate: (String, NavOptions) -> Unit ) { - TopAppBar( - title = { - Text( - account?.user?.name - ?: account?.server?.name - ?: "Meteroid" + Surface( + modifier = Modifier.padding(8.dp), + color = MaterialTheme.colorScheme.surface, + shadowElevation = 6.dp, + tonalElevation = 6.dp, + shape = RoundedCornerShape(32.dp), + onClick = { + onNavigate(Routes.Users.List, NavOptions.Builder().build()) + } + ) { + Row(modifier = Modifier.padding(8.dp)) { + AsyncImage( + account?.user?.gravatarUrl(), + contentDescription = "User List", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.tertiary) ) - }, - navigationIcon = { - AsyncImage( - account?.user?.gravatarUrl(), - contentDescription = "User List", - contentScale = ContentScale.Crop, - modifier = Modifier - .padding(start = 6.dp, end = 24.dp) - .size(40.dp) - .clip(CircleShape) - .border(1.dp, Color.White, CircleShape) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - }, - actions = { + Spacer(Modifier.width(16.dp)) + Column(modifier = Modifier.align(Alignment.CenterVertically)) { + if (account != null) { + if (account.user != null) { + Text( + account.user!!.name, + fontWeight = FontWeight.SemiBold + ) + Text( + account.server.url.toHttpUrl().host, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f), + fontWeight = FontWeight.Medium + ) + } else { + Text( + account.server.url.toHttpUrl().host, + fontWeight = FontWeight.SemiBold + ) + } + } else { + Text( + "Meteroid", + fontWeight = FontWeight.SemiBold + ) + } + } + Spacer( + Modifier + .weight(1.0f) + .width(16.dp)) account?.user?.let { user -> - val (foreground, background) = - if (user.balance < 0) - Pair(MaterialTheme.colorScheme.onError, MaterialTheme.colorScheme.error) - else - Pair(MaterialTheme.colorScheme.onPrimary, MaterialTheme.colorScheme.primary) - - Text( - String.format("%.02f €", user.balance), - color = foreground, - fontSize = 14.sp, - lineHeight = 20.sp, - fontWeight = FontWeight.SemiBold, + PriceBadge( + user.balance, modifier = Modifier - .padding(end = 20.dp) - .clip(RoundedCornerShape(16.dp)) - .background(background) - .padding(horizontal = 8.dp) + .align(Alignment.CenterVertically) + .padding(end = 12.dp) ) } - }, - modifier = Modifier - .clickable { - onNavigate( - Routes.Users.Root, - NavOptions - .Builder() - .build() - ) - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer - ) - ) + } + } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt index 3689ee0d1a02a3b9c7cf08dabb87f01c9e308822..7950b83ed59f9bed43d443a29b2801bc66ba6da2 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt @@ -27,7 +27,7 @@ package de.chaosdorf.meteroid.ui.servers import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import de.chaosdorf.mete.v1.MeteApiV1Factory +import de.chaosdorf.mete.model.MeteApiFactory import de.chaosdorf.meteroid.model.Server import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.model.ServerRepository @@ -44,6 +44,7 @@ import kotlin.time.Duration.Companion.milliseconds @HiltViewModel class AddServerViewModel @Inject constructor( + private val factory: MeteApiFactory, private val repository: ServerRepository ) : ViewModel() { val url = MutableStateFlow("") @@ -52,7 +53,7 @@ class AddServerViewModel @Inject constructor( id: ServerId, url: String ): Server? = try { - val api = MeteApiV1Factory.newInstance(url) + val api = factory.newInstance(url) val manifest = api.getManifest() val icon = manifest?.findBestIcon() Server( diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseListItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt similarity index 77% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseListItem.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt index ed56a75c8623e0e0dca5d6d33c6ef73bfe48d6b4..5b5c6682244098d71a4154223e682a1b4b8ed50b 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseListItem.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt @@ -22,7 +22,7 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.purchases +package de.chaosdorf.meteroid.ui.transactions import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AttachMoney import androidx.compose.material.icons.filled.QuestionMark @@ -45,32 +44,33 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import coil.compose.rememberAsyncImagePainter import de.chaosdorf.meteroid.model.Drink -import de.chaosdorf.meteroid.model.Purchase +import de.chaosdorf.meteroid.model.Transaction +import de.chaosdorf.meteroid.ui.PriceBadge import kotlinx.datetime.TimeZone import kotlinx.datetime.toJavaLocalDateTime import kotlinx.datetime.toLocalDateTime +import java.math.BigDecimal import java.time.format.DateTimeFormatter import java.time.format.FormatStyle @Composable -fun PurchaseListItem( - purchase: Purchase, +fun TransactionListItem( + transaction: Transaction, drink: Drink? ) { - val timestamp = purchase.createdAt.toLocalDateTime(TimeZone.currentSystemDefault()) + val timestamp = transaction.timestamp.toLocalDateTime(TimeZone.currentSystemDefault()) val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT) ListItem( headlineContent = { - val label = - if (drink != null) drink.name - else if (purchase.difference > 0.0) "Deposit" - else "Unknown" + val label = when { + drink != null -> drink.name + transaction.difference > BigDecimal.ZERO -> "Deposit" + else -> "Unknown" + } Text(label) }, supportingContent = { @@ -101,7 +101,7 @@ fun PurchaseListItem( .align(Alignment.Center) .fillMaxSize() ) - } else if (purchase.difference > 0) { + } else if (transaction.difference > BigDecimal.ZERO) { Icon( Icons.Default.AttachMoney, contentDescription = null, @@ -117,22 +117,9 @@ fun PurchaseListItem( } }, trailingContent = { - val (foreground, background) = - if (purchase.difference < 0) - Pair(MaterialTheme.colorScheme.onError, MaterialTheme.colorScheme.error) - else - Pair(MaterialTheme.colorScheme.onPrimary, MaterialTheme.colorScheme.primary) - - Text( - String.format("%.02f €", purchase.difference), - color = foreground, - fontSize = 14.sp, - lineHeight = 20.sp, - fontWeight = FontWeight.SemiBold, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .background(background) - .padding(horizontal = 8.dp) + PriceBadge( + transaction.difference, + modifier = Modifier.padding(horizontal = 8.dp) ) } ) diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt similarity index 77% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseListScreen.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt index ac31c251b4430f1931cebf1154a7eb811019f6bb..d6ceedbf2aa979303fb700761e513fc192a9a620 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseListScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt @@ -22,33 +22,31 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.purchases +package de.chaosdorf.meteroid.ui.Transactions -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.navigation.NavOptions -import de.chaosdorf.meteroid.sync.SyncHandler import de.chaosdorf.meteroid.ui.navigation.HomeSections import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar +import de.chaosdorf.meteroid.ui.transactions.TransactionListItem +import de.chaosdorf.meteroid.ui.transactions.TransactionViewModel @Composable -fun PurchaseListScreen( - viewModel: PurchaseViewModel, +fun TransactionListScreen( + viewModel: TransactionViewModel, onNavigate: (String, NavOptions) -> Unit ) { val account by viewModel.account.collectAsState() - val purchases by viewModel.purchases.collectAsState() - val syncState by viewModel.syncState.collectAsState() + val transactions by viewModel.transactions.collectAsState() Scaffold( topBar = { MeteroidTopBar(account, onNavigate) }, @@ -60,14 +58,9 @@ fun PurchaseListScreen( ) } ) { paddingValues: PaddingValues -> - Column { - if (syncState == SyncHandler.State.Loading) { - LinearProgressIndicator() - } - LazyColumn(modifier = Modifier.padding(paddingValues)) { - items(purchases) { (purchase, drink) -> - PurchaseListItem(purchase, drink) - } + LazyColumn(contentPadding = paddingValues) { + items(transactions) { (transaction, drink) -> + TransactionListItem(transaction, drink) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt similarity index 70% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseViewModel.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt index b31def9bf795ae8b62529c0c4680f752af141976..8c053ee161ae6d11ec671834086164a549df2494 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt @@ -22,17 +22,17 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.purchases +package de.chaosdorf.meteroid.ui.transactions import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import de.chaosdorf.meteroid.model.AccountInfo import de.chaosdorf.meteroid.model.DrinkRepository -import de.chaosdorf.meteroid.model.PurchaseRepository +import de.chaosdorf.meteroid.model.TransactionRepository import de.chaosdorf.meteroid.sync.AccountProvider -import de.chaosdorf.meteroid.sync.PurchaseSyncHandler import de.chaosdorf.meteroid.sync.SyncHandler +import de.chaosdorf.meteroid.sync.TransactionSyncHandler import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -40,31 +40,31 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn +import java.math.BigDecimal import javax.inject.Inject import kotlin.time.Duration.Companion.minutes @HiltViewModel -class PurchaseViewModel @Inject constructor( +class TransactionViewModel @Inject constructor( accountProvider: AccountProvider, - repository: PurchaseRepository, - drinkRepository: DrinkRepository, - syncHandler: PurchaseSyncHandler + repository: TransactionRepository, + drinkRepository: DrinkRepository ) : ViewModel() { val account: StateFlow<AccountInfo?> = accountProvider.account .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) - val purchases: StateFlow<List<PurchaseInfo>> = accountProvider.account + val transactions: StateFlow<List<TransactionInfo>> = accountProvider.account .flatMapLatest { account -> account?.let { (server, maybeUser) -> maybeUser?.let { user -> combine( repository.getAllFlow(server.serverId, user.userId), drinkRepository.getAllFlow(server.serverId) - ) { purchases, drinks -> - purchases.map { purchase -> - PurchaseInfo( - purchase, - drinks.firstOrNull { drink -> drink.drinkId == purchase.drinkId } + ) { transactions, drinks -> + transactions.map { transaction -> + TransactionInfo( + transaction, + drinks.firstOrNull { drink -> drink.drinkId == transaction.drinkId } ) } } @@ -72,28 +72,26 @@ class PurchaseViewModel @Inject constructor( } ?: flowOf(emptyList()) }.mapLatest { list -> list.mergeAdjecentDeposits() - .filter { it.drink != null || it.purchase.difference != 0.0 } + .filter { it.drink != null || it.transaction.difference != BigDecimal.ZERO } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) - - val syncState: StateFlow<SyncHandler.State> = syncHandler.state } -fun List<PurchaseInfo>.mergeAdjecentDeposits(): List<PurchaseInfo> { - val result = mutableListOf<PurchaseInfo>() +fun List<TransactionInfo>.mergeAdjecentDeposits(): List<TransactionInfo> { + val result = mutableListOf<TransactionInfo>() for (entry in this) { val previous = result.lastOrNull() if (previous != null - && previous.purchase.difference > 0 - && entry.purchase.difference > 0 + && previous.transaction.difference > BigDecimal.ZERO + && entry.transaction.difference > BigDecimal.ZERO && previous.drink == null && entry.drink == null - && entry.purchase.createdAt.minus(previous.purchase.createdAt) < 5.minutes + && entry.transaction.timestamp.minus(previous.transaction.timestamp) < 5.minutes ) { result.removeLast() result.add( entry.copy( - purchase = entry.purchase.copy( - difference = entry.purchase.difference + previous.purchase.difference + transaction = entry.transaction.copy( + difference = entry.transaction.difference + previous.transaction.difference ) ) ) diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseInfo.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/TransactionInfo.kt similarity index 88% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseInfo.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/TransactionInfo.kt index b389efebf5249dae8238e488e42b24749469e09a..c4654ce50a48a99930da29c83ba6431a7c78d504 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/purchases/PurchaseInfo.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/TransactionInfo.kt @@ -22,12 +22,12 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.purchases +package de.chaosdorf.meteroid.ui.transactions import de.chaosdorf.meteroid.model.Drink -import de.chaosdorf.meteroid.model.Purchase +import de.chaosdorf.meteroid.model.Transaction -data class PurchaseInfo( - val purchase: Purchase, +data class TransactionInfo( + val transaction: Transaction, val drink: Drink? ) diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt index 02b12293813fd0cb1ca876516ce480d8f47e6ab8..a6890ee323a8d65496333ce696ced87a530c2699 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt @@ -27,13 +27,11 @@ package de.chaosdorf.meteroid.ui.users import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -49,8 +47,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import coil.compose.AsyncImage -import de.chaosdorf.mete.UserId -import de.chaosdorf.meteroid.sync.SyncHandler +import de.chaosdorf.mete.model.UserId @Composable fun UserListScreen( @@ -61,7 +58,6 @@ fun UserListScreen( ) { val server by viewModel.account.collectAsState() val users by viewModel.users.collectAsState() - val syncState by viewModel.syncState.collectAsState() Scaffold( topBar = { @@ -90,24 +86,19 @@ fun UserListScreen( ) } ) { paddingValues -> - Column { - if (syncState == SyncHandler.State.Loading) { - LinearProgressIndicator() + LazyColumn(modifier = Modifier.padding(paddingValues)) { + items(users) { user -> + ListItem( + headlineContent = { Text(user.name) }, + supportingContent = { Text(user.email) }, + modifier = Modifier.clickable { onSelect(user.userId) } + ) } - LazyColumn(modifier = Modifier.padding(paddingValues)) { - items(users) { user -> - ListItem( - headlineContent = { Text(user.name) }, - supportingContent = { Text(user.email) }, - modifier = Modifier.clickable { onSelect(user.userId) } - ) - } - item { - ListItem( - headlineContent = { Text("Add User") }, - modifier = Modifier.clickable { onAdd() } - ) - } + item { + ListItem( + headlineContent = { Text("Add User") }, + modifier = Modifier.clickable { onAdd() } + ) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt index bc36b148b7208947c54043e425431fdb9d9443fe..74029fd8617672231a6a33a36615c34b30941317 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt @@ -28,11 +28,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import de.chaosdorf.meteroid.model.AccountInfo -import de.chaosdorf.meteroid.model.Server import de.chaosdorf.meteroid.model.User import de.chaosdorf.meteroid.model.UserRepository import de.chaosdorf.meteroid.sync.AccountProvider -import de.chaosdorf.meteroid.sync.SyncHandler import de.chaosdorf.meteroid.sync.UserSyncHandler import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -44,8 +42,7 @@ import javax.inject.Inject @HiltViewModel class UserListViewModel @Inject constructor( accountProvider: AccountProvider, - repository: UserRepository, - syncHandler: UserSyncHandler + repository: UserRepository ) : ViewModel() { val account: StateFlow<AccountInfo?> = accountProvider.account .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) @@ -56,6 +53,4 @@ class UserListViewModel @Inject constructor( repository.getAllFlow(server.serverId) } ?: flowOf(emptyList()) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) - - val syncState: StateFlow<SyncHandler.State> = syncHandler.state } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt index cd7ecd4dc8fddc4a5adc7f90df3ed638fea08372..65416dbf694bbcba2b3dfae7e1c48af95aa2d147 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt @@ -24,8 +24,8 @@ package de.chaosdorf.meteroid.util -import de.chaosdorf.mete.PwaIcon -import de.chaosdorf.mete.PwaManifest +import de.chaosdorf.mete.model.PwaIcon +import de.chaosdorf.mete.model.PwaManifest import okhttp3.HttpUrl.Companion.toHttpUrl fun PwaManifest.findBestIcon(): PwaIcon? = icons.maxByOrNull { diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/Update.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/Update.kt new file mode 100644 index 0000000000000000000000000000000000000000..f3578153c03f72e8598b58ce6260b6257e6913f5 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/Update.kt @@ -0,0 +1,35 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.util + +import androidx.lifecycle.SavedStateHandle + +fun <T> SavedStateHandle.update(key: String, block: (T?) -> T) { + this[key] = block(this[key]) +} + +fun <T> SavedStateHandle.update(key: String, default: T, block: (T) -> T) { + this[key] = block(this[key] ?: default) +} diff --git a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json index b8a0cbb9f2075143fb44088bef045f59af22274f..9efb6d66778c527a3cf0e91a61123aa799eca474 100644 --- a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json +++ b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "957bb839c1c422a256c594595437ee45", + "identityHash": "7aca7cdc33cbadb81643b32c5838f2a9", "entities": [ { "tableName": "Drink", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `drinkId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` REAL NOT NULL, `caffeine` INTEGER, `price` REAL NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `logoUrl` TEXT NOT NULL, `logoFileName` TEXT NOT NULL, `logoContentType` TEXT NOT NULL, `logoFileSize` INTEGER NOT NULL, `logoUpdatedAt` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `drinkId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `drinkId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `volume` TEXT NOT NULL, `caffeine` INTEGER, `price` TEXT NOT NULL, `logoUrl` TEXT NOT NULL, PRIMARY KEY(`serverId`, `drinkId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "serverId", @@ -35,7 +35,7 @@ { "fieldPath": "volume", "columnName": "volume", - "affinity": "REAL", + "affinity": "TEXT", "notNull": true }, { @@ -47,19 +47,7 @@ { "fieldPath": "price", "columnName": "price", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "createdAt", - "columnName": "createdAt", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "updatedAt", - "columnName": "updatedAt", - "affinity": "INTEGER", + "affinity": "TEXT", "notNull": true }, { @@ -67,30 +55,6 @@ "columnName": "logoUrl", "affinity": "TEXT", "notNull": true - }, - { - "fieldPath": "logoFileName", - "columnName": "logoFileName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "logoContentType", - "columnName": "logoContentType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "logoFileSize", - "columnName": "logoFileSize", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "logoUpdatedAt", - "columnName": "logoUpdatedAt", - "affinity": "INTEGER", - "notNull": true } ], "primaryKey": { @@ -155,7 +119,7 @@ }, { "tableName": "User", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `balance` REAL NOT NULL, `audit` INTEGER NOT NULL, `redirect` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `userId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `active` INTEGER NOT NULL, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `balance` TEXT NOT NULL, `audit` INTEGER NOT NULL, `redirect` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `userId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "serverId", @@ -190,7 +154,7 @@ { "fieldPath": "balance", "columnName": "balance", - "affinity": "REAL", + "affinity": "TEXT", "notNull": true }, { @@ -204,18 +168,6 @@ "columnName": "redirect", "affinity": "INTEGER", "notNull": true - }, - { - "fieldPath": "createdAt", - "columnName": "createdAt", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "updatedAt", - "columnName": "updatedAt", - "affinity": "INTEGER", - "notNull": true } ], "primaryKey": { @@ -241,8 +193,8 @@ ] }, { - "tableName": "Purchase", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `purchaseId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `drinkId` INTEGER, `difference` REAL NOT NULL, `createdAt` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `purchaseId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`serverId`, `userId`) REFERENCES `User`(`serverId`, `userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`serverId`, `drinkId`) REFERENCES `Drink`(`serverId`, `drinkId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "tableName": "Transaction", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `transactionId` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `drinkId` INTEGER, `difference` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `transactionId`), FOREIGN KEY(`serverId`) REFERENCES `Server`(`serverId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`serverId`, `userId`) REFERENCES `User`(`serverId`, `userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`serverId`, `drinkId`) REFERENCES `Drink`(`serverId`, `drinkId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", "fields": [ { "fieldPath": "serverId", @@ -251,8 +203,8 @@ "notNull": true }, { - "fieldPath": "purchaseId", - "columnName": "purchaseId", + "fieldPath": "transactionId", + "columnName": "transactionId", "affinity": "INTEGER", "notNull": true }, @@ -271,12 +223,12 @@ { "fieldPath": "difference", "columnName": "difference", - "affinity": "REAL", + "affinity": "TEXT", "notNull": true }, { - "fieldPath": "createdAt", - "columnName": "createdAt", + "fieldPath": "timestamp", + "columnName": "timestamp", "affinity": "INTEGER", "notNull": true } @@ -285,29 +237,29 @@ "autoGenerate": false, "columnNames": [ "serverId", - "purchaseId" + "transactionId" ] }, "indices": [ { - "name": "index_Purchase_serverId_userId", + "name": "index_Transaction_serverId_userId", "unique": false, "columnNames": [ "serverId", "userId" ], "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Purchase_serverId_userId` ON `${TABLE_NAME}` (`serverId`, `userId`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_Transaction_serverId_userId` ON `${TABLE_NAME}` (`serverId`, `userId`)" }, { - "name": "index_Purchase_serverId_drinkId", + "name": "index_Transaction_serverId_drinkId", "unique": false, "columnNames": [ "serverId", "drinkId" ], "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_Purchase_serverId_drinkId` ON `${TABLE_NAME}` (`serverId`, `drinkId`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_Transaction_serverId_drinkId` ON `${TABLE_NAME}` (`serverId`, `drinkId`)" } ], "foreignKeys": [ @@ -354,7 +306,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '957bb839c1c422a256c594595437ee45')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7aca7cdc33cbadb81643b32c5838f2a9')" ] } } \ No newline at end of file diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt index a8b32864c10b8d73627ebc960cbe1809b224a17e..8cf5fac65c306c37e61882c2a458597da1c18c95 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt @@ -29,12 +29,13 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverters import de.chaosdorf.meteroid.model.Drink import de.chaosdorf.meteroid.model.DrinkRepository -import de.chaosdorf.meteroid.model.Purchase -import de.chaosdorf.meteroid.model.PurchaseRepository import de.chaosdorf.meteroid.model.Server import de.chaosdorf.meteroid.model.ServerRepository +import de.chaosdorf.meteroid.model.Transaction +import de.chaosdorf.meteroid.model.TransactionRepository import de.chaosdorf.meteroid.model.User import de.chaosdorf.meteroid.model.UserRepository +import de.chaosdorf.meteroid.util.BigDecimalTypeConverter import de.chaosdorf.meteroid.util.KotlinDatetimeTypeConverter @Database( @@ -43,14 +44,19 @@ import de.chaosdorf.meteroid.util.KotlinDatetimeTypeConverter Drink::class, Server::class, User::class, - Purchase::class + Transaction::class ], autoMigrations = [], ) -@TypeConverters(value = [KotlinDatetimeTypeConverter::class]) +@TypeConverters( + value = [ + KotlinDatetimeTypeConverter::class, + BigDecimalTypeConverter::class + ] +) abstract class MeteroidDatabase : RoomDatabase() { abstract fun drinks(): DrinkRepository abstract fun server(): ServerRepository abstract fun users(): UserRepository - abstract fun purchases(): PurchaseRepository + abstract fun transactions(): TransactionRepository } diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt index ade8587945246de5bcbb0d1adf2a93e3933e6cc2..6ea48860e6b2ff82b43e85f391aff1db39e75022 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt @@ -30,10 +30,10 @@ import androidx.room.ForeignKey import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import de.chaosdorf.mete.DrinkId -import de.chaosdorf.mete.v1.DrinkModelV1 +import de.chaosdorf.mete.model.DrinkId +import de.chaosdorf.mete.model.DrinkModel import kotlinx.coroutines.flow.Flow -import kotlinx.datetime.Instant +import java.math.BigDecimal import java.net.URI @Entity( @@ -47,33 +47,21 @@ data class Drink( val drinkId: DrinkId, val active: Boolean, val name: String, - val volume: Double, + val volume: BigDecimal, val caffeine: Int?, - val price: Double, - val createdAt: Instant, - val updatedAt: Instant, + val price: BigDecimal, val logoUrl: String, - val logoFileName: String, - val logoContentType: String, - val logoFileSize: Long, - val logoUpdatedAt: Instant ) { companion object { - fun fromModelV1(server: Server, value: DrinkModelV1) = Drink( + fun fromModel(server: Server, value: DrinkModel) = Drink( server.serverId, - value.id, + value.drinkId, value.active, value.name, - value.bottleSize, + value.volume, value.caffeine, value.price, - value.createdAt, - value.updatedAt, - URI.create(server.url).resolve(value.logoUrl).toString(), - value.logoFileName, - value.logoContentType, - value.logoFileSize, - value.logoUpdatedAt + URI.create(server.url).resolve(value.logoUrl).toString() ) } } @@ -89,7 +77,7 @@ interface DrinkRepository { @Query("SELECT * FROM Drink WHERE serverId = :serverId ORDER BY NAME ASC") suspend fun getAll(serverId: ServerId): List<Drink> - @Query("SELECT * FROM Drink WHERE serverId = :serverId ORDER BY NAME ASC") + @Query("SELECT * FROM Drink WHERE serverId = :serverId ORDER BY ACTIVE DESC, NAME ASC") fun getAllFlow(serverId: ServerId): Flow<List<Drink>> @Insert(onConflict = OnConflictStrategy.REPLACE) diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Purchase.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt similarity index 57% rename from persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Purchase.kt rename to persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt index fad3d86e350b4c7afd02239cdb84c17a82577a9f..c564bbdfb01c9543f815cd373b9384e64d1af3e1 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Purchase.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt @@ -31,62 +31,73 @@ import androidx.room.Index import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import de.chaosdorf.mete.AuditEntryId -import de.chaosdorf.mete.DrinkId -import de.chaosdorf.mete.UserId -import de.chaosdorf.mete.v1.AuditEntryModelV1 +import de.chaosdorf.mete.model.DrinkId +import de.chaosdorf.mete.model.TransactionId +import de.chaosdorf.mete.model.TransactionModel +import de.chaosdorf.mete.model.UserId import kotlinx.coroutines.flow.Flow import kotlinx.datetime.Instant +import java.math.BigDecimal @Entity( - primaryKeys = ["serverId", "purchaseId"], + primaryKeys = ["serverId", "transactionId"], foreignKeys = [ ForeignKey(Server::class, ["serverId"], ["serverId"], onDelete = ForeignKey.CASCADE), - ForeignKey(User::class, ["serverId", "userId"], ["serverId", "userId"], onDelete = ForeignKey.CASCADE), - ForeignKey(Drink::class, ["serverId", "drinkId"], ["serverId", "drinkId"], onDelete = ForeignKey.NO_ACTION) + ForeignKey( + User::class, + ["serverId", "userId"], + ["serverId", "userId"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + Drink::class, + ["serverId", "drinkId"], + ["serverId", "drinkId"], + onDelete = ForeignKey.NO_ACTION + ) ], indices = [ Index("serverId", "userId"), Index("serverId", "drinkId") ] ) -data class Purchase( +data class Transaction( val serverId: ServerId, - val purchaseId: AuditEntryId, + val transactionId: TransactionId, val userId: UserId, val drinkId: DrinkId?, - val difference: Double, - val createdAt: Instant + val difference: BigDecimal, + val timestamp: Instant ) { companion object { - fun fromModelV1(server: Server, userId: UserId, value: AuditEntryModelV1) = Purchase( + fun fromModel(server: Server, userId: UserId, value: TransactionModel) = Transaction( server.serverId, - value.id, + value.transactionId, userId, - value.drink, + value.drinkId, value.difference, - value.createdAt + value.timestamp ) } } @Dao -interface PurchaseRepository { - @Query("SELECT * FROM Purchase WHERE serverId = :serverId AND userId = :userId ORDER BY createdAt DESC") - suspend fun getAll(serverId: ServerId, userId: UserId): List<Purchase> +interface TransactionRepository { + @Query("SELECT * FROM `Transaction` WHERE serverId = :serverId AND userId = :userId ORDER BY timestamp DESC") + suspend fun getAll(serverId: ServerId, userId: UserId): List<Transaction> - @Query("SELECT * FROM Purchase WHERE serverId = :serverId AND userId = :userId ORDER BY createdAt DESC") - fun getAllFlow(serverId: ServerId, userId: UserId): Flow<List<Purchase>> + @Query("SELECT * FROM `Transaction` WHERE serverId = :serverId AND userId = :userId ORDER BY timestamp DESC") + fun getAllFlow(serverId: ServerId, userId: UserId): Flow<List<Transaction>> @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(purchase: Purchase) + suspend fun save(transaction: Transaction) - @Query("DELETE FROM Purchase WHERE serverId = :serverId AND purchaseId = :purchaseId") - suspend fun delete(serverId: ServerId, purchaseId: AuditEntryId) + @Query("DELETE FROM `Transaction` WHERE serverId = :serverId AND transactionId = :transactionId") + suspend fun delete(serverId: ServerId, transactionId: TransactionId) - @Query("DELETE FROM Purchase WHERE serverId = :serverId AND userId = :userId") + @Query("DELETE FROM `Transaction` WHERE serverId = :serverId AND userId = :userId") suspend fun deleteAll(serverId: ServerId, userId: UserId) - @Query("DELETE FROM Purchase WHERE serverId = :serverId") + @Query("DELETE FROM `Transaction` WHERE serverId = :serverId") suspend fun deleteAll(serverId: ServerId) } diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt index 40d52aeec9d3096a47b95eab34a97349e02ae123..60561680db069844b9498cba1ef618947febd417 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt @@ -30,10 +30,10 @@ import androidx.room.ForeignKey import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import de.chaosdorf.mete.UserId -import de.chaosdorf.mete.v1.UserModelV1 +import de.chaosdorf.mete.model.UserId +import de.chaosdorf.mete.model.UserModel import kotlinx.coroutines.flow.Flow -import kotlinx.datetime.Instant +import java.math.BigDecimal import java.security.MessageDigest import java.util.Locale @@ -49,24 +49,20 @@ data class User( val active: Boolean, val name: String, val email: String, - val balance: Double, + val balance: BigDecimal, val audit: Boolean, val redirect: Boolean, - val createdAt: Instant, - val updatedAt: Instant, ) { companion object { - fun fromModelV1(server: Server, value: UserModelV1) = User( + fun fromModel(server: Server, value: UserModel) = User( server.serverId, - value.id, + value.userId, value.active, value.name, value.email, value.balance, value.audit, value.redirect, - value.createdAt, - value.updatedAt ) } diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/BigDecimalTypeConverter.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/BigDecimalTypeConverter.kt new file mode 100644 index 0000000000000000000000000000000000000000..4e46f64becdddc56a4dee9f27782facaa3fbe118 --- /dev/null +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/BigDecimalTypeConverter.kt @@ -0,0 +1,36 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.meteroid.util + +import androidx.room.TypeConverter +import java.math.BigDecimal + +class BigDecimalTypeConverter { + @TypeConverter + fun load(value: String): BigDecimal = BigDecimal(value) + + @TypeConverter + fun store(value: BigDecimal): String = value.toString() +}