feat: add product selection step before meal planning
Inserts a new PlanProductsSheet as step 1 of the planning flow. Users see their current products as a multi-select checklist (all selected by default) before choosing the planning mode and dates. - Empty state explains the benefit and offers "Add products" CTA while always allowing "Plan without products" to skip - Selected product IDs flow through PlanMenuSheet → PlanDatePickerSheet → MenuService.generateForDates → backend - Backend: added ProductIDs field to generate-menu request body; uses ListForPromptByIDs when set, ListForPrompt otherwise - Backend: added Repository.ListForPromptByIDs (filtered SQL query) - All 12 ARB locale files updated with planProducts* keys Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,8 @@ type UserLoader interface {
|
||||
// ProductLister returns human-readable product lines for the AI prompt.
|
||||
type ProductLister interface {
|
||||
ListForPrompt(ctx context.Context, userID string) ([]string, error)
|
||||
// ListForPromptByIDs returns only the products with the given IDs.
|
||||
ListForPromptByIDs(ctx context.Context, userID string, ids []string) ([]string, error)
|
||||
}
|
||||
|
||||
// RecipeSaver creates a dish+recipe and returns the new recipe ID.
|
||||
@@ -121,9 +123,10 @@ func (h *Handler) GenerateMenu(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Week string `json:"week"` // optional, defaults to current week
|
||||
Dates []string `json:"dates"` // YYYY-MM-DD; triggers partial generation
|
||||
MealTypes []string `json:"meal_types"` // overrides user preference when set
|
||||
Week string `json:"week"` // optional, defaults to current week
|
||||
Dates []string `json:"dates"` // YYYY-MM-DD; triggers partial generation
|
||||
MealTypes []string `json:"meal_types"` // overrides user preference when set
|
||||
ProductIDs []string `json:"product_ids"` // when set, only these products are passed to AI
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||
|
||||
@@ -136,7 +139,7 @@ func (h *Handler) GenerateMenu(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if len(body.Dates) > 0 {
|
||||
h.generateForDates(w, r, userID, u, body.Dates, body.MealTypes)
|
||||
h.generateForDates(w, r, userID, u, body.Dates, body.MealTypes, body.ProductIDs)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -150,8 +153,14 @@ func (h *Handler) GenerateMenu(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
menuReq := buildMenuRequest(u, locale.FromContext(r.Context()))
|
||||
|
||||
if products, productError := h.productLister.ListForPrompt(r.Context(), userID); productError == nil {
|
||||
menuReq.AvailableProducts = products
|
||||
if len(body.ProductIDs) > 0 {
|
||||
if products, productError := h.productLister.ListForPromptByIDs(r.Context(), userID, body.ProductIDs); productError == nil {
|
||||
menuReq.AvailableProducts = products
|
||||
}
|
||||
} else {
|
||||
if products, productError := h.productLister.ListForPrompt(r.Context(), userID); productError == nil {
|
||||
menuReq.AvailableProducts = products
|
||||
}
|
||||
}
|
||||
|
||||
days, generateError := h.menuGenerator.GenerateMenu(r.Context(), menuReq)
|
||||
@@ -194,7 +203,7 @@ func (h *Handler) GenerateMenu(w http.ResponseWriter, r *http.Request) {
|
||||
// generateForDates handles partial menu generation for specific dates and meal types.
|
||||
// It groups dates by ISO week, generates only the requested slots, upserts them
|
||||
// without touching other existing slots, and returns {"plans":[...]}.
|
||||
func (h *Handler) generateForDates(w http.ResponseWriter, r *http.Request, userID string, u *user.User, dates, requestedMealTypes []string) {
|
||||
func (h *Handler) generateForDates(w http.ResponseWriter, r *http.Request, userID string, u *user.User, dates, requestedMealTypes, productIDs []string) {
|
||||
mealTypes := requestedMealTypes
|
||||
if len(mealTypes) == 0 {
|
||||
// Fall back to user's preferred meal types.
|
||||
@@ -212,8 +221,14 @@ func (h *Handler) generateForDates(w http.ResponseWriter, r *http.Request, userI
|
||||
|
||||
menuReq := buildMenuRequest(u, locale.FromContext(r.Context()))
|
||||
menuReq.MealTypes = mealTypes
|
||||
if products, productError := h.productLister.ListForPrompt(r.Context(), userID); productError == nil {
|
||||
menuReq.AvailableProducts = products
|
||||
if len(productIDs) > 0 {
|
||||
if products, productError := h.productLister.ListForPromptByIDs(r.Context(), userID, productIDs); productError == nil {
|
||||
menuReq.AvailableProducts = products
|
||||
}
|
||||
} else {
|
||||
if products, productError := h.productLister.ListForPrompt(r.Context(), userID); productError == nil {
|
||||
menuReq.AvailableProducts = products
|
||||
}
|
||||
}
|
||||
|
||||
weekGroups := groupDatesByWeek(dates)
|
||||
|
||||
Reference in New Issue
Block a user