feat: implement Iteration 4 — menu planning, shopping list, diary

Backend:
- Migrations 007 (menu_plans, menu_items, shopping_lists) and
  008 (meal_diary)
- gemini/menu.go: GenerateMenu — 7-day × 3-meal plan via one Groq call
- internal/menu: model, repository (GetByWeek, SaveMenuInTx,
  shopping list CRUD), handler (GET/PUT/DELETE /menu,
  POST /ai/generate-menu, shopping list endpoints)
- internal/diary: model, repository, handler (GET/POST/DELETE /diary)
- Increase server WriteTimeout to 120s for long AI calls
- api_client.go: add patch() and postList() helpers

Flutter:
- shared/models: menu.dart, shopping_item.dart, diary_entry.dart
- features/menu: menu_service.dart, menu_provider.dart
  (MenuNotifier, ShoppingListNotifier, DiaryNotifier with family)
- MenuScreen: 7-day view, week nav, skeleton on generation,
  generate FAB with confirmation dialog
- ShoppingListScreen: items by category, optimistic checkbox toggle
- DiaryScreen: daily entries with swipe-to-delete, add-entry sheet
- Router: /menu/shopping-list and /menu/diary routes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dbastrikin
2026-02-22 12:00:25 +02:00
parent 612a0eda60
commit ea8e207a45
22 changed files with 2926 additions and 12 deletions

View File

@@ -5,7 +5,9 @@ import (
"net/http"
"github.com/food-ai/backend/internal/auth"
"github.com/food-ai/backend/internal/diary"
"github.com/food-ai/backend/internal/ingredient"
"github.com/food-ai/backend/internal/menu"
"github.com/food-ai/backend/internal/middleware"
"github.com/food-ai/backend/internal/product"
"github.com/food-ai/backend/internal/recognition"
@@ -25,6 +27,8 @@ func NewRouter(
ingredientHandler *ingredient.Handler,
productHandler *product.Handler,
recognitionHandler *recognition.Handler,
menuHandler *menu.Handler,
diaryHandler *diary.Handler,
authMiddleware func(http.Handler) http.Handler,
allowedOrigins []string,
) *chi.Mux {
@@ -44,16 +48,12 @@ func NewRouter(
r.Post("/logout", authHandler.Logout)
})
// Public search (still requires auth to prevent scraping)
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/ingredients/search", ingredientHandler.Search)
})
// Protected
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/ingredients/search", ingredientHandler.Search)
r.Get("/profile", userHandler.Get)
r.Put("/profile", userHandler.Update)
@@ -74,10 +74,29 @@ func NewRouter(
r.Delete("/{id}", productHandler.Delete)
})
r.Route("/menu", func(r chi.Router) {
r.Get("/", menuHandler.GetMenu)
r.Put("/items/{id}", menuHandler.UpdateMenuItem)
r.Delete("/items/{id}", menuHandler.DeleteMenuItem)
})
r.Route("/shopping-list", func(r chi.Router) {
r.Get("/", menuHandler.GetShoppingList)
r.Post("/generate", menuHandler.GenerateShoppingList)
r.Patch("/items/{index}/check", menuHandler.ToggleShoppingItem)
})
r.Route("/diary", func(r chi.Router) {
r.Get("/", diaryHandler.GetByDate)
r.Post("/", diaryHandler.Create)
r.Delete("/{id}", diaryHandler.Delete)
})
r.Route("/ai", func(r chi.Router) {
r.Post("/recognize-receipt", recognitionHandler.RecognizeReceipt)
r.Post("/recognize-products", recognitionHandler.RecognizeProducts)
r.Post("/recognize-dish", recognitionHandler.RecognizeDish)
r.Post("/generate-menu", menuHandler.GenerateMenu)
})
})