diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 9a6539dd..e8420eb6 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,6 +1,6 @@ name: Run Tests -on: [ pull_request_target ] +on: [ pull_request ] env: GITHUB_ACTOR: ${{ github.actor }} @@ -33,8 +33,8 @@ jobs: path: | ~/.gradle/caches ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: gradle-${{ runner.os }} + key: gradle-v2-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }} + restore-keys: gradle-v2-${{ runner.os }} - name: Run Tests run: ./gradlew test --no-daemon diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index d8dde792..70660768 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -110,7 +110,6 @@ kotlin { implementation(compose.preview) implementation(libs.androidx.activity.compose) implementation(libs.koin.android) - implementation(project.dependencies.platform(libs.android.firebase.bom)) } commonMain.dependencies { implementation(compose.runtime) @@ -184,6 +183,10 @@ android { } dependencies { debugImplementation(compose.uiTooling) + implementation(platform(libs.android.firebase.bom)) + implementation("com.google.firebase:firebase-auth") + implementation("com.google.firebase:firebase-common") + implementation("com.google.firebase:firebase-firestore") } } dependencies { diff --git a/composeApp/src/commonMain/kotlin/modules/ViewModelModules.kt b/composeApp/src/commonMain/kotlin/modules/ViewModelModules.kt index 02a705a2..9df4e227 100644 --- a/composeApp/src/commonMain/kotlin/modules/ViewModelModules.kt +++ b/composeApp/src/commonMain/kotlin/modules/ViewModelModules.kt @@ -18,14 +18,14 @@ import view.admin.recipes.RecipeManagementViewModel val viewModelModules = module { single { ViewModelEventOverview(get(), get()) } - single { CategorizedShoppingListViewModel(get(), get()) } + single { CategorizedShoppingListViewModel(get(), get(), get()) } single { ViewModelNewParticipant(get()) } single { SharedEventViewModel(get(), get(), get()) } single { RecipeViewModel(get()) } single { IngredientViewModel(get()) } single { AllParticipantsViewModel(get()) } single { RecipeOverviewViewModel(get(), get()) } - single { MaterialListViewModel(get(), get()) } + single { MaterialListViewModel(get(), get(), get()) } single { CsvImportViewModel(get(), get()) } single { CookingGroupIngredientsViewModel(get()) } single { RecipeManagementViewModel(get(), get()) } diff --git a/composeApp/src/commonMain/kotlin/view/event/categorized_shopping_list/CategorizedShoppingListViewModel.kt b/composeApp/src/commonMain/kotlin/view/event/categorized_shopping_list/CategorizedShoppingListViewModel.kt index 916b942b..bdde3644 100644 --- a/composeApp/src/commonMain/kotlin/view/event/categorized_shopping_list/CategorizedShoppingListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/view/event/categorized_shopping_list/CategorizedShoppingListViewModel.kt @@ -28,7 +28,8 @@ data class ShoppingListState( class CategorizedShoppingListViewModel( private val calculateShoppingList: CalculateShoppingList, - private val eventRepository: EventRepository + private val eventRepository: EventRepository, + private val pdfServiceModule: services.pdfService.PdfServiceModule ) : ViewModel() { @@ -66,6 +67,8 @@ class CategorizedShoppingListViewModel( editShoppingListActions.date ) + is EditShoppingListActions.ExportPdf -> exportPdf() + } } catch (e: Exception) { _state.value = ResultState.Error("Fehler beim laden der Einkaufsliste") @@ -237,5 +240,12 @@ class CategorizedShoppingListViewModel( ) ) } + + private fun exportPdf() { + val successData = state.value.getSuccessData() ?: return + viewModelScope.launch { + pdfServiceModule.createPdf(successData.eventId) + } + } } diff --git a/composeApp/src/commonMain/kotlin/view/event/categorized_shopping_list/EditShoppingListActions.kt b/composeApp/src/commonMain/kotlin/view/event/categorized_shopping_list/EditShoppingListActions.kt index e3ef5a1c..5d4c0323 100644 --- a/composeApp/src/commonMain/kotlin/view/event/categorized_shopping_list/EditShoppingListActions.kt +++ b/composeApp/src/commonMain/kotlin/view/event/categorized_shopping_list/EditShoppingListActions.kt @@ -16,4 +16,5 @@ interface EditShoppingListActions : BaseAction { class DeleteShoppingItem(val shoppingIngredient: ShoppingIngredient) : EditShoppingListActions { } + data object ExportPdf : EditShoppingListActions } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/view/event/categorized_shopping_list/ShoppingListCategorized.kt b/composeApp/src/commonMain/kotlin/view/event/categorized_shopping_list/ShoppingListCategorized.kt index 7a32dea8..24aba6b3 100644 --- a/composeApp/src/commonMain/kotlin/view/event/categorized_shopping_list/ShoppingListCategorized.kt +++ b/composeApp/src/commonMain/kotlin/view/event/categorized_shopping_list/ShoppingListCategorized.kt @@ -28,6 +28,8 @@ import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.KeyboardArrowLeft import androidx.compose.material.icons.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.Save +import androidx.compose.material.icons.filled.Share import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -49,6 +51,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController +import getPlatformName import kotlinx.datetime.LocalDate import model.Ingredient import model.MultiDayShoppingList @@ -105,16 +108,34 @@ fun ShoppingListCategorized( items = ingredientList, onItemAdded = { text -> onAction(EditShoppingListActions.AddNewIngredient(text)) }, topBar = { - TopAppBar(title = { - Text(text = "Einkaufsliste") - }, navigationIcon = { - NavigationIconButton( - onLeave = { - onAction(EditShoppingListActions.SaveToEvent) - onAction(NavigationActions.GoBack) + TopAppBar( + title = { Text(text = "Einkaufsliste") }, + navigationIcon = { + NavigationIconButton( + onLeave = { + onAction(EditShoppingListActions.SaveToEvent) + onAction(NavigationActions.GoBack) + } + ) + }, + actions = { + IconButton( + onClick = { onAction(EditShoppingListActions.ExportPdf) }, + modifier = Modifier.clip(RoundedCornerShape(75)) + .background(MaterialTheme.colorScheme.tertiary) + ) { + val imageVector = when (getPlatformName()) { + "desktop" -> Icons.Default.Save + else -> Icons.Default.Share + } + Icon( + imageVector = imageVector, + contentDescription = "Export PDF", + tint = MaterialTheme.colorScheme.onTertiary + ) } - ) - }) + } + ) }, content = { Column( diff --git a/composeApp/src/commonMain/kotlin/view/event/materiallist/EditMaterialListActions.kt b/composeApp/src/commonMain/kotlin/view/event/materiallist/EditMaterialListActions.kt index ed4da0ba..62f562ef 100644 --- a/composeApp/src/commonMain/kotlin/view/event/materiallist/EditMaterialListActions.kt +++ b/composeApp/src/commonMain/kotlin/view/event/materiallist/EditMaterialListActions.kt @@ -9,4 +9,5 @@ interface EditMaterialListActions : BaseAction { data class Add(val materialName: String) : EditMaterialListActions data class Delete(val material: Material) : EditMaterialListActions data object SaveMaterialList : EditMaterialListActions + data object ExportPdf : EditMaterialListActions } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/view/event/materiallist/MaterialListScreen.kt b/composeApp/src/commonMain/kotlin/view/event/materiallist/MaterialListScreen.kt index 8df46888..24e658c1 100644 --- a/composeApp/src/commonMain/kotlin/view/event/materiallist/MaterialListScreen.kt +++ b/composeApp/src/commonMain/kotlin/view/event/materiallist/MaterialListScreen.kt @@ -11,8 +11,12 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.background +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Save +import androidx.compose.material.icons.filled.Share import androidx.compose.material3.Card import androidx.compose.material3.IconButton import androidx.compose.material3.Icon @@ -28,8 +32,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.Modifier +import getPlatformName import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController @@ -84,17 +90,34 @@ fun MaterialList( }, onItemAdded = { text -> onAction(EditMaterialListActions.Add(text)) }, topBar = { - TopAppBar(title = { - Text(text = "Materialliste") - }, navigationIcon = { - NavigationIconButton( - onLeave = { - onAction(EditMaterialListActions.SaveMaterialList) - onAction(NavigationActions.GoBack) + TopAppBar( + title = { Text(text = "Materialliste") }, + navigationIcon = { + NavigationIconButton( + onLeave = { + onAction(EditMaterialListActions.SaveMaterialList) + onAction(NavigationActions.GoBack) + } + ) + }, + actions = { + IconButton( + onClick = { onAction(EditMaterialListActions.ExportPdf) }, + modifier = Modifier.clip(RoundedCornerShape(75)) + .background(MaterialTheme.colorScheme.tertiary) + ) { + val imageVector = when (getPlatformName()) { + "desktop" -> Icons.Default.Save + else -> Icons.Default.Share + } + Icon( + imageVector = imageVector, + contentDescription = "Export PDF", + tint = MaterialTheme.colorScheme.onTertiary + ) } - - ) - }) + } + ) }) } diff --git a/composeApp/src/commonMain/kotlin/view/event/materiallist/MaterialListViewModel.kt b/composeApp/src/commonMain/kotlin/view/event/materiallist/MaterialListViewModel.kt index d97c4c83..6f895b4c 100644 --- a/composeApp/src/commonMain/kotlin/view/event/materiallist/MaterialListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/view/event/materiallist/MaterialListViewModel.kt @@ -27,7 +27,8 @@ data class MaterialListState( class MaterialListViewModel( private val calculateMaterialList: CalculateMaterialList, - private val eventRepository: EventRepository + private val eventRepository: EventRepository, + private val pdfServiceModule: services.pdfService.PdfServiceModule ) : ViewModel() { @@ -55,6 +56,10 @@ class MaterialListViewModel( is EditMaterialListActions.Delete -> { deleteMaterial(materialListActions.material) } + + is EditMaterialListActions.ExportPdf -> { + exportPdf() + } } } catch (e: Exception) { _state.value = ResultState.Error("Fehler beim Laden der Materialliste") @@ -124,5 +129,12 @@ class MaterialListViewModel( ) } } + + private fun exportPdf() { + val state = _state.value.getSuccessData() ?: return + viewModelScope.launch { + pdfServiceModule.createPdf(state.eventId) + } + } } diff --git a/composeApp/src/commonMain/kotlin/view/event/new_event/NewEventPage.kt b/composeApp/src/commonMain/kotlin/view/event/new_event/NewEventPage.kt index c5e70558..4542276e 100644 --- a/composeApp/src/commonMain/kotlin/view/event/new_event/NewEventPage.kt +++ b/composeApp/src/commonMain/kotlin/view/event/new_event/NewEventPage.kt @@ -338,7 +338,7 @@ private fun ShoppingAndMaterialList( } Button( onClick = { - onAction(EditEventActions.ShareRecipePlanPdf) + onAction(NavigationActions.GoToRoute(Routes.RecipePlan)) }, modifier = Modifier.padding(8.dp).height(IntrinsicSize.Min), colors = ButtonColors( @@ -381,103 +381,6 @@ fun TopBarEventPage( onAction(NavigationActions.GoBack) onAction(EditEventActions.SaveEvent) }) - }, actions = { - Row( - horizontalArrangement = Arrangement.End, - ) { - - IconButton( - onClick = { - if (sharedState is ResultState.Success) { - onAction(EditShoppingListActions.Initialize(sharedState.data.event.uid)) - onAction( - NavigationActions.GoToRoute( - Routes.ShoppingList(sharedState.data.event.uid) - ) - ) - } - }, - modifier = Modifier.clip(shape = RoundedCornerShape(75)) - .background(MaterialTheme.colorScheme.secondary), - - ) { - Icon( - imageVector = Icons.Default.ShoppingCart, - contentDescription = "Shopping Cart Icon", - tint = MaterialTheme.colorScheme.onSecondary - ) - } - Spacer(modifier = Modifier.width(8.dp)) - ShareRecipePlanPdfButton(onAction) - Spacer(modifier = Modifier.width(8.dp)) - SharePdfButton(onAction) - } }) } - -@Composable -private fun SharePdfButton(onAction: (BaseAction) -> Unit) { - var isButtonEnabled by remember { mutableStateOf(true) } - - LaunchedEffect(isButtonEnabled) { - if (isButtonEnabled) return@LaunchedEffect - else delay(2000L) - isButtonEnabled = true - } - - IconButton( - onClick = { - if (isButtonEnabled) { - isButtonEnabled = false - onAction(EditEventActions.SharePdf) - } - }, - enabled = isButtonEnabled, - modifier = Modifier.clip(shape = RoundedCornerShape(75)) - .background(MaterialTheme.colorScheme.tertiary), - ) { - val imageVector = when (getPlatformName()) { - "desktop" -> Icons.Default.Save - else -> Icons.Default.Share - } - Icon( - imageVector = imageVector, - contentDescription = "Printer Icon", - tint = MaterialTheme.colorScheme.onTertiary - ) - - } -} - -@Composable -private fun ShareRecipePlanPdfButton(onAction: (BaseAction) -> Unit) { - var isButtonEnabled by remember { mutableStateOf(true) } - - LaunchedEffect(isButtonEnabled) { - if (isButtonEnabled) return@LaunchedEffect - else delay(2000L) - isButtonEnabled = true - } - - IconButton( - onClick = { - if (isButtonEnabled) { - isButtonEnabled = false - onAction(EditEventActions.ShareRecipePlanPdf) - } - }, - enabled = isButtonEnabled, - modifier = Modifier.clip(shape = RoundedCornerShape(75)) - .background(MaterialTheme.colorScheme.tertiary), - ) { - Icon( - imageVector = Icons.Default.RestaurantMenu, - contentDescription = "Rezeptplan exportieren", - tint = MaterialTheme.colorScheme.onTertiary - ) - } -} - - - diff --git a/composeApp/src/commonMain/kotlin/view/event/recipe_plan/RecipePlanScreen.kt b/composeApp/src/commonMain/kotlin/view/event/recipe_plan/RecipePlanScreen.kt new file mode 100644 index 00000000..ae2797aa --- /dev/null +++ b/composeApp/src/commonMain/kotlin/view/event/recipe_plan/RecipePlanScreen.kt @@ -0,0 +1,197 @@ +package view.event.recipe_plan + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Save +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavHostController +import getPlatformName +import kotlinx.datetime.LocalDate +import model.Meal +import org.koin.compose.koinInject +import view.event.EventState +import view.event.SharedEventViewModel +import view.event.actions.BaseAction +import view.event.actions.EditEventActions +import view.event.actions.NavigationActions +import view.event.actions.handleNavigation +import view.login.ErrorField +import view.shared.HelperFunctions +import view.shared.MGCircularProgressIndicator +import view.shared.NavigationIconButton +import view.shared.ResultState +import view.shared.page.ColumnWithPadding + +@Composable +fun RecipePlanScreen(navController: NavHostController) { + val sharedEventViewModel: SharedEventViewModel = koinInject() + val sharedState = sharedEventViewModel.eventState.collectAsStateWithLifecycle() + + RecipePlanPage( + state = sharedState.value, + onAction = { action -> + when (action) { + is NavigationActions -> handleNavigation(navController, action) + is EditEventActions -> sharedEventViewModel.onAction(action) + } + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RecipePlanPage( + state: ResultState, + onAction: (BaseAction) -> Unit +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = "Rezeptplan") }, + navigationIcon = { + NavigationIconButton( + onLeave = { onAction(NavigationActions.GoBack) } + ) + }, + actions = { + IconButton( + onClick = { onAction(EditEventActions.ShareRecipePlanPdf) }, + modifier = Modifier.clip(RoundedCornerShape(75)) + .background(MaterialTheme.colorScheme.tertiary) + ) { + val imageVector = when (getPlatformName()) { + "desktop" -> Icons.Default.Save + else -> Icons.Default.Share + } + Icon( + imageVector = imageVector, + contentDescription = "Export PDF", + tint = MaterialTheme.colorScheme.onTertiary + ) + } + } + ) + } + ) { paddingValues -> + Surface( + color = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.fillMaxSize().padding(paddingValues) + ) { + when (state) { + is ResultState.Success -> { + RecipePlanContent( + eventName = state.data.event.name, + mealsGroupedByDate = state.data.mealsGroupedByDate + ) + } + + is ResultState.Loading -> { + ColumnWithPadding { MGCircularProgressIndicator() } + } + + is ResultState.Error -> { + ColumnWithPadding { ErrorField(state.message) } + } + } + } + } +} + +@Composable +fun RecipePlanContent( + eventName: String, + mealsGroupedByDate: Map> +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .verticalScroll(rememberScrollState()) + ) { + Text( + text = eventName, + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(bottom = 16.dp) + ) + + mealsGroupedByDate.forEach { (date, meals) -> + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = HelperFunctions.formatDate(date), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(8.dp)) + + meals.forEach { meal -> + MealItem(meal = meal) + } + } + } + } + } +} + +@Composable +fun MealItem(meal: Meal) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + ) { + Text( + text = meal.mealType.name, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + if (meal.recipeSelections.isNotEmpty()) { + meal.recipeSelections.forEach { selection -> + val personCount = selection.eaterIds.size + selection.guestCount + Text( + text = " • ${selection.selectedRecipeName} (${personCount} Personen)", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + } else { + Text( + text = " Kein Rezept", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/view/navigation/RootNavController.kt b/composeApp/src/commonMain/kotlin/view/navigation/RootNavController.kt index 5861cdf2..3ac30df6 100644 --- a/composeApp/src/commonMain/kotlin/view/navigation/RootNavController.kt +++ b/composeApp/src/commonMain/kotlin/view/navigation/RootNavController.kt @@ -17,6 +17,7 @@ import view.admin.recipes.RecipeManagementScreen import view.event.categorized_shopping_list.MaterialListScreen import view.event.categorized_shopping_list.ShoppingListScreen import view.event.homescreen.EventOverviewScreen +import view.event.recipe_plan.RecipePlanScreen import view.event.new_event.NewEventScreen import view.event.new_meal_screen.EditMealScreen import view.event.participants.ParticipantScreen @@ -104,6 +105,9 @@ fun RootNavController( composable { MaterialListScreen(navController) } + composable { + RecipePlanScreen(navController) + } composable { CookingGroupsScreen(navController) } diff --git a/composeApp/src/commonMain/kotlin/view/navigation/Routes.kt b/composeApp/src/commonMain/kotlin/view/navigation/Routes.kt index 12ac7ee4..564e0235 100644 --- a/composeApp/src/commonMain/kotlin/view/navigation/Routes.kt +++ b/composeApp/src/commonMain/kotlin/view/navigation/Routes.kt @@ -24,6 +24,9 @@ interface Routes { @Serializable object MaterialList : Routes + @Serializable + object RecipePlan : Routes + @Serializable class RecipeOverview(val recipeRef: String) : Routes diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8af5b62b..211dfcec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] agp = "8.10.1" -android-compileSdk = "35" +android-compileSdk = "36" android-minSdk = "24" -android-targetSdk = "35" +android-targetSdk = "36" androidx-activityCompose = "1.11.0" androidx-appcompat = "1.7.1" androidx-constraintlayout = "2.2.1" @@ -26,7 +26,7 @@ kotlinx-serialization = "1.7.3" pdfbox = "3.0.2" google-services = "4.4.3" firebase-bom = "34.3.0" -gitlive = "2.1.0" +gitlive = "2.4.0" junitJupiter = "5.13.3" buildkonfig = "0.16.0" compose-hot-reload = "1.0.0-alpha03"