feat: improve meals with better search and ids #5

Merged
bissendorf merged 1 commits from feat/improve-meals into dev 2025-07-20 23:00:08 +00:00
5 changed files with 74 additions and 42 deletions

View File

@ -83,6 +83,7 @@ func getVerbs(ctx context.Context, config *AppConfig) (commands []*cobra.Command
Use: r.Name, Use: r.Name,
Aliases: r.Aliases, Aliases: r.Aliases,
Short: r.Description, Short: r.Description,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// Configure log // Configure log
var logLevel = slog.LevelWarn var logLevel = slog.LevelWarn
@ -120,6 +121,7 @@ func getVerbs(ctx context.Context, config *AppConfig) (commands []*cobra.Command
param.Description, param.Description,
) )
} }
verbCommand.AddCommand(resourceCommand) verbCommand.AddCommand(resourceCommand)
} }

View File

@ -3,6 +3,8 @@ package meals
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"strings"
"time" "time"
"git.bissendorf.co/bissendorf/unifood/m/v2/core/handler" "git.bissendorf.co/bissendorf/unifood/m/v2/core/handler"
@ -22,47 +24,68 @@ type MealsHandler struct {
func (h *MealsHandler) Get(ctx context.Context, name string, params params.Container) (*interfaces.ResourceList, error) { func (h *MealsHandler) Get(ctx context.Context, name string, params params.Container) (*interfaces.ResourceList, error) {
// Read parameters // Read parameters
p, err := params.GetValue(handler.ParamDate) date, err := params.GetValue(handler.ParamDate)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to parse date parameter: %w", err) return nil, fmt.Errorf("unable to parse date parameter: %w", err)
} }
date := p.(time.Time)
location, err := params.GetValue(handler.ParamLocation) restaurant, err := params.GetValue(handler.ParamRestaurant)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to parse location parameter: %w", err) return nil, fmt.Errorf("unable to parse restaurant parameter: %w", err)
} }
// Build query // Build query
query := fmt.Sprintf( query := "page('meals').children.filterBy('printonly', 0)"
`page('meals').children.filterBy('location', '%s').filterBy('date', '%s').filterBy('printonly', 0)`, if date != "" {
location, parsed, err := time.Parse(time.DateOnly, date.(string))
date.Format(time.DateOnly), if err != nil {
) return nil, fmt.Errorf("unable to parse date parameter: %w", err)
}
query += fmt.Sprintf(".filterBy('date', '%s')", parsed.Format(time.DateOnly))
}
if restaurant != "" {
query += fmt.Sprintf(".filterBy('location', '%s')", restaurant.(string))
}
if name != "" {
query += fmt.Sprintf(".filterBy('id', 'meals/%s')", name)
}
// Run query // Run query
meals, err := h.QueryClient.Get(ctx, meals, err := h.QueryClient.Get(ctx,
query, query,
`{"title":true,"ingredients":"page.ingredients.toObject","prices":"page.prices.toObject","location":true,"counter":true,"date":true,"mealadds":true,"mark":true,"frei3":true,"printonly":true,"kombicategory":true,"categories":"page.categories.split"}`, `{"title":true,"id":"page.id","ingredients":"page.ingredients.toObject","prices":"page.prices.toObject","location":true,"counter":true,"date":true,"mealadds":true,"mark":true,"frei3":true,"printonly":true,"kombicategory":true,"categories":"page.categories.split"}`,
false, false,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("querying menu failed: %w", err) return nil, fmt.Errorf("querying menu failed: %w", err)
} }
// Transform to meal resource
items := util.Transform(*meals, func(i *stwbremen.Meal) *resources.Meal {
d, err := resources.MealFromDTO(*i)
if err != nil {
return &resources.Meal{}
}
return d
})
// Sort by date, restaurant and title - need to apply stable sorts in reverse order to achieve this
slices.SortFunc(items, func(a, b *resources.Meal) int {
return strings.Compare(a.Title, b.Title)
})
slices.SortStableFunc(items, func(a, b *resources.Meal) int {
return strings.Compare(a.RestaurantID, b.RestaurantID)
})
slices.SortStableFunc(items, func(a, b *resources.Meal) int {
return a.Date.Compare(b.Date)
})
// Return // Return
return &interfaces.ResourceList{ return &interfaces.ResourceList{
ItemKind: resources.ResourceMeal, ItemKind: resources.ResourceMeal,
Items: util.Transform(*meals, func(i *stwbremen.Meal) interfaces.Resource { Items: util.Transform(items, func(i **resources.Meal) interfaces.Resource { return *i }),
d, err := resources.MealFromDTO(*i)
if err != nil {
return &resources.Meal{}
}
return d
}),
}, nil }, nil
} }
func (h *MealsHandler) GetParametersForVerb(verb interfaces.Verb) []params.Registration { func (h *MealsHandler) GetParametersForVerb(verb interfaces.Verb) []params.Registration {
@ -71,14 +94,14 @@ func (h *MealsHandler) GetParametersForVerb(verb interfaces.Verb) []params.Regis
Name: handler.ParamDate, Name: handler.ParamDate,
ShortHand: "d", ShortHand: "d",
Description: "Meal date", Description: "Meal date",
DefaultFunc: func() string { return time.Now().Format(time.DateOnly) }, DefaultFunc: func() string { return "" },
ParseFunc: func(value string) (any, error) { return time.Parse(time.DateOnly, value) }, ParseFunc: params.ParseString,
}, },
{ {
Name: handler.ParamLocation, Name: handler.ParamRestaurant,
ShortHand: "l", ShortHand: "r",
Description: "Select a restaurant", Description: "Select a restaurant",
DefaultFunc: func() string { return "330" }, DefaultFunc: func() string { return "" },
ParseFunc: params.ParseString, ParseFunc: params.ParseString,
}, },
} }

View File

@ -1,6 +1,6 @@
package handler package handler
const ( const (
ParamDate string = "date" ParamDate string = "date"
ParamLocation string = "location" ParamRestaurant string = "restaurant"
) )

View File

@ -2,6 +2,7 @@ package stwbremen
type Meal struct { type Meal struct {
Title string `json:"title"` Title string `json:"title"`
ID string `json:"id"`
Ingredients []Ingredient `json:"ingredients"` Ingredients []Ingredient `json:"ingredients"`
Prices []Price `json:"prices"` Prices []Price `json:"prices"`
Location string `json:"location"` Location string `json:"location"`

View File

@ -16,12 +16,15 @@ func MealFromDTO(meal stwbremen.Meal) (*Meal, error) {
return nil, fmt.Errorf("unable to parse meal date: %w", err) return nil, fmt.Errorf("unable to parse meal date: %w", err)
} }
id, _ := strings.CutPrefix(meal.ID, "meals/")
return &Meal{ return &Meal{
Title: meal.Title, Title: meal.Title,
Location: meal.Location, RestaurantID: meal.Location,
Date: date, ID: id,
Tags: strings.Split(strings.Replace(meal.Tags, " ", "", -1), ","), Date: date,
Counter: meal.Counter, Tags: strings.Split(strings.Replace(meal.Tags, " ", "", -1), ","),
Counter: meal.Counter,
Prices: util.Map(meal.Prices, func(i *stwbremen.Price) (string, float32) { Prices: util.Map(meal.Prices, func(i *stwbremen.Price) (string, float32) {
p, err := strconv.ParseFloat(strings.Trim(i.Price, " "), 32) p, err := strconv.ParseFloat(strings.Trim(i.Price, " "), 32)
if err != nil { if err != nil {
@ -41,13 +44,14 @@ func MealFromDTO(meal stwbremen.Meal) (*Meal, error) {
const ResourceMeal = "meal" const ResourceMeal = "meal"
type Meal struct { type Meal struct {
Title string Title string
Location string ID string
Ingredients []ingredient RestaurantID string
Prices map[string]float32 Ingredients []ingredient
Date time.Time Prices map[string]float32
Counter string Date time.Time
Tags []string Counter string
Tags []string
} }
type ingredient struct { type ingredient struct {
@ -56,10 +60,12 @@ type ingredient struct {
} }
func (d *Meal) Kind() string { return ResourceMeal } func (d *Meal) Kind() string { return ResourceMeal }
func (d *Meal) ItemName() string { return d.Title } func (d *Meal) ItemName() string { return d.ID }
// Table output // Table output
func (d *Meal) ColumnNames() []string { return []string{"Location", "Date", "Counter", "Price"} } func (d *Meal) ColumnNames() []string {
func (d *Meal) Columns() []any { return []string{"Date", "Restaurant", "Title", "Counter", "Price"}
return []any{d.Location, d.Date.Format(time.DateOnly), d.Counter, d.Prices["Studierende"]} }
func (d *Meal) Columns() []any {
return []any{d.Date.Format(time.DateOnly), d.RestaurantID, d.Title, d.Counter, d.Prices["Studierende"]}
} }