diff --git a/frontend/assets/css/main.css b/frontend/assets/css/main.css index 160ed0d6dc58..6b272d54f7fd 100644 --- a/frontend/assets/css/main.css +++ b/frontend/assets/css/main.css @@ -47,3 +47,21 @@ .v-card__title { word-break: normal !important; } + +.grid-cols-3 { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-gap: 1rem; +} + +@media screen and (max-width: 960px) { + .grid-cols-3 { + grid-template-columns: 1fr 1fr; + } +} + +@media screen and (max-width: 600px) { + .grid-cols-3 { + grid-template-columns: 1fr; + } +} diff --git a/frontend/components/Domain/Recipe/RecipeOrganizerPage.vue b/frontend/components/Domain/Recipe/RecipeOrganizerPage.vue index 9779cacfb1d2..0bd04bae08c6 100644 --- a/frontend/components/Domain/Recipe/RecipeOrganizerPage.vue +++ b/frontend/components/Domain/Recipe/RecipeOrganizerPage.vue @@ -24,24 +24,32 @@ -
- - - - - - - {{ icon }} - - - {{ item.name }} - - - - - - - +
+ +
+ + + + + {{ item.count }} + + + {{ icon }} + + + {{ item.name }} + + + + + +
@@ -56,6 +64,7 @@ interface GenericItem { id?: string; name: string; slug: string; + count?: number; } export default defineComponent({ @@ -80,7 +89,7 @@ export default defineComponent({ // ================================================================= // Sorted Items const itemsSorted = computed(() => { - const byLetter: { [key: string]: Array } = {}; + const byLetter: Record> = {}; if (!props.items) return byLetter; @@ -133,3 +142,9 @@ export default defineComponent({ head: {}, }); + + diff --git a/mealie/repos/repository_factory.py b/mealie/repos/repository_factory.py index 761f0ea5c30c..8bc8efa1fd56 100644 --- a/mealie/repos/repository_factory.py +++ b/mealie/repos/repository_factory.py @@ -1,5 +1,6 @@ from functools import cached_property +from sqlalchemy import func from sqlalchemy.orm import Session from mealie.db.models.group import Group, GroupMealPlan, ReportEntryModel, ReportModel @@ -47,11 +48,12 @@ from mealie.schema.labels import MultiPurposeLabelOut from mealie.schema.meal_plan.new_meal import ReadPlanEntry from mealie.schema.meal_plan.plan_rules import PlanRulesOut from mealie.schema.recipe import Recipe, RecipeCommentOut, RecipeToolOut -from mealie.schema.recipe.recipe_category import CategoryOut, TagOut +from mealie.schema.recipe.recipe_category import CategoryCount, CategoryOut, TagCount, TagOut from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit from mealie.schema.recipe.recipe_share_token import RecipeShareToken from mealie.schema.recipe.recipe_timeline_events import RecipeTimelineEventOut from mealie.schema.reports.reports import ReportEntryOut, ReportOut +from mealie.schema.response.pagination import PaginationBase, PaginationQuery from mealie.schema.server import ServerTask from mealie.schema.user import GroupInDB, LongLiveTokenInDB, PrivateUser from mealie.schema.user.user_passwords import PrivatePasswordResetToken @@ -73,11 +75,57 @@ class RepositoryCategories(RepositoryGeneric[CategoryOut, Category]): def get_empty(self): return self.session.query(Category).filter(~Category.recipes.any()).all() + def get_all_count_recipes(self, pagination: PaginationQuery) -> PaginationBase[TagCount]: + q = self.session.query(Category, func.count(RecipeModel.id).filter(RecipeModel.recipe_category)).group_by( + Category.id + ) + + fltr = self._filter_builder() + q = q.filter_by(**fltr) + q, count, total_pages = self.add_pagination_to_query(q, pagination) + + try: + data: list[tuple[Category, int]] = q.all() + except Exception as e: + self._log_exception(e) + self.session.rollback() + raise e + + return PaginationBase( + page=pagination.page, + per_page=pagination.per_page, + total=count, + total_pages=total_pages, + items=[CategoryCount(**s[0].__dict__, count=s[1]) for s in data], + ) + class RepositoryTags(RepositoryGeneric[TagOut, Tag]): def get_empty(self): return self.session.query(Tag).filter(~Tag.recipes.any()).all() + def get_all_count_recipes(self, pagination: PaginationQuery) -> PaginationBase[TagCount]: + q = self.session.query(self.model, func.count(RecipeModel.id)).join(RecipeModel.tags).group_by(self.model) + + fltr = self._filter_builder() + q = q.filter_by(**fltr) + q, count, total_pages = self.add_pagination_to_query(q, pagination) + + try: + data: list[tuple[Tag, int]] = q.all() + except Exception as e: + self._log_exception(e) + self.session.rollback() + raise e + + return PaginationBase( + page=pagination.page, + per_page=pagination.per_page, + total=count, + total_pages=total_pages, + items=[TagCount(**s[0].__dict__, count=s[1]) for s in data], + ) + class AllRepositories: def __init__(self, session: Session) -> None: diff --git a/mealie/routes/organizers/controller_categories.py b/mealie/routes/organizers/controller_categories.py index 3e584225b940..8e4ea9d7b227 100644 --- a/mealie/routes/organizers/controller_categories.py +++ b/mealie/routes/organizers/controller_categories.py @@ -7,9 +7,9 @@ from mealie.routes._base import BaseCrudController, controller from mealie.routes._base.mixins import HttpRepo from mealie.schema import mapper from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse -from mealie.schema.recipe.recipe import RecipeCategory, RecipeCategoryPagination -from mealie.schema.recipe.recipe_category import CategoryBase, CategorySave -from mealie.schema.response.pagination import PaginationQuery +from mealie.schema.recipe.recipe import RecipeCategory +from mealie.schema.recipe.recipe_category import CategoryBase, CategoryCount, CategorySave +from mealie.schema.response.pagination import PaginationBase, PaginationQuery from mealie.services import urls from mealie.services.event_bus_service.event_types import EventCategoryData, EventOperation, EventTypes @@ -37,13 +37,10 @@ class RecipeCategoryController(BaseCrudController): def mixins(self): return HttpRepo(self.repo, self.logger) - @router.get("", response_model=RecipeCategoryPagination) + @router.get("", response_model=PaginationBase[CategoryCount]) def get_all(self, q: PaginationQuery = Depends(PaginationQuery)): """Returns a list of available categories in the database""" - response = self.repo.page_all( - pagination=q, - override=RecipeCategory, - ) + response = self.repo.get_all_count_recipes(pagination=q) response.set_pagination_guides(router.url_path_for("get_all"), q.dict()) return response diff --git a/mealie/routes/organizers/controller_tags.py b/mealie/routes/organizers/controller_tags.py index 5f993890dd2f..e438154d3f17 100644 --- a/mealie/routes/organizers/controller_tags.py +++ b/mealie/routes/organizers/controller_tags.py @@ -3,11 +3,12 @@ from functools import cached_property from fastapi import APIRouter, Depends, HTTPException, status from pydantic import UUID4 +from mealie.repos.repository_factory import RepositoryTags from mealie.routes._base import BaseCrudController, controller from mealie.routes._base.mixins import HttpRepo from mealie.schema import mapper from mealie.schema.recipe import RecipeTagResponse, TagIn -from mealie.schema.recipe.recipe import RecipeTag, RecipeTagPagination +from mealie.schema.recipe.recipe import RecipeTagPagination from mealie.schema.recipe.recipe_category import TagSave from mealie.schema.response.pagination import PaginationQuery from mealie.services import urls @@ -19,7 +20,7 @@ router = APIRouter(prefix="/tags", tags=["Organizer: Tags"]) @controller(router) class TagController(BaseCrudController): @cached_property - def repo(self): + def repo(self) -> RepositoryTags: return self.repos.tags.by_group(self.group_id) @cached_property @@ -29,10 +30,7 @@ class TagController(BaseCrudController): @router.get("", response_model=RecipeTagPagination) async def get_all(self, q: PaginationQuery = Depends(PaginationQuery)): """Returns a list of available tags in the database""" - response = self.repo.page_all( - pagination=q, - override=RecipeTag, - ) + response = self.repo.get_all_count_recipes(pagination=q) response.set_pagination_guides(router.url_path_for("get_all"), q.dict()) return response diff --git a/mealie/routes/recipe/all_recipe_routes.py b/mealie/routes/recipe/all_recipe_routes.py index d910f548ef29..540f39dc2506 100644 --- a/mealie/routes/recipe/all_recipe_routes.py +++ b/mealie/routes/recipe/all_recipe_routes.py @@ -4,6 +4,7 @@ from sqlalchemy.orm.session import Session from mealie.db.db_setup import generate_session from mealie.repos.all_repositories import get_repositories from mealie.schema.recipe import RecipeSummary +from mealie.schema.response.pagination import PaginationQuery router = APIRouter() @@ -18,3 +19,13 @@ async def get_untagged_recipes(count: bool = False, session: Session = Depends(g async def get_uncategorized_recipes(count: bool = False, session: Session = Depends(generate_session)): db = get_repositories(session) return db.recipes.count_uncategorized(count=count, override_schema=RecipeSummary) + + +@router.get("/summar/whatever") +async def get_whatever(session: Session = Depends(generate_session)): + db = get_repositories(session) + payload = db.tags.get_all_count_recipes( + PaginationQuery(), + ) + + return payload diff --git a/mealie/schema/recipe/__init__.py b/mealie/schema/recipe/__init__.py index 8b97aa3ea85e..e49302d1173c 100644 --- a/mealie/schema/recipe/__init__.py +++ b/mealie/schema/recipe/__init__.py @@ -5,7 +5,6 @@ from .recipe import ( CreateRecipeByUrlBulk, Recipe, RecipeCategory, - RecipeCategoryPagination, RecipePagination, RecipePaginationQuery, RecipeSummary, diff --git a/mealie/schema/recipe/recipe_category.py b/mealie/schema/recipe/recipe_category.py index a8d4ceaf3f76..497e1885955b 100644 --- a/mealie/schema/recipe/recipe_category.py +++ b/mealie/schema/recipe/recipe_category.py @@ -53,7 +53,7 @@ class TagBase(CategoryBase): pass -class TagOut(TagSave): +class TagOut(TagIn): id: UUID4 slug: str @@ -61,6 +61,17 @@ class TagOut(TagSave): orm_mode = True +class TagCount(TagOut): + count: int = 0 + + class Config: + orm_mode = True + + +class CategoryCount(TagCount): + ... + + class RecipeTagResponse(RecipeCategoryResponse): pass