fix: show dish calories in search and fix portion sheet layout crash
- DishSearchResult now carries calories_per_serving (backend entity + repo LEFT JOIN recipes / MIN / GROUP BY; Flutter model + fromJson) - _FoodTile.fromDish shows kcal/serving subtitle when available - _DishPortionSheet quick-select buttons: Row → Wrap to avoid BoxConstraints infinite-width crash inside DraggableScrollableSheet Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,7 @@ type DishSearchResult struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ImageURL *string `json:"image_url,omitempty"`
|
ImageURL *string `json:"image_url,omitempty"`
|
||||||
AvgRating float64 `json:"avg_rating"`
|
AvgRating float64 `json:"avg_rating"`
|
||||||
|
CaloriesPerServing *float64 `json:"calories_per_serving,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRequest is the body used to create a new dish + recipe at once.
|
// CreateRequest is the body used to create a new dish + recipe at once.
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ func (r *Repository) Search(ctx context.Context, query string, limit int) ([]*Di
|
|||||||
COALESCE(dt.name, d.name) AS name,
|
COALESCE(dt.name, d.name) AS name,
|
||||||
d.image_url,
|
d.image_url,
|
||||||
d.avg_rating,
|
d.avg_rating,
|
||||||
|
MIN(r.calories_per_serving) AS calories_per_serving,
|
||||||
GREATEST(
|
GREATEST(
|
||||||
ts_rank(to_tsvector('english', d.name), plainto_tsquery('english', $1)),
|
ts_rank(to_tsvector('english', d.name), plainto_tsquery('english', $1)),
|
||||||
ts_rank(to_tsvector('simple', COALESCE(dt.name, '')), plainto_tsquery('simple', $1)),
|
ts_rank(to_tsvector('simple', COALESCE(dt.name, '')), plainto_tsquery('simple', $1)),
|
||||||
@@ -118,6 +119,7 @@ func (r *Repository) Search(ctx context.Context, query string, limit int) ([]*Di
|
|||||||
) AS rank
|
) AS rank
|
||||||
FROM dishes d
|
FROM dishes d
|
||||||
LEFT JOIN dish_translations dt ON dt.dish_id = d.id AND dt.lang = $3
|
LEFT JOIN dish_translations dt ON dt.dish_id = d.id AND dt.lang = $3
|
||||||
|
LEFT JOIN recipes r ON r.dish_id = d.id
|
||||||
WHERE (
|
WHERE (
|
||||||
to_tsvector('english', d.name) @@ plainto_tsquery('english', $1)
|
to_tsvector('english', d.name) @@ plainto_tsquery('english', $1)
|
||||||
OR to_tsvector('simple', COALESCE(dt.name, '')) @@ plainto_tsquery('simple', $1)
|
OR to_tsvector('simple', COALESCE(dt.name, '')) @@ plainto_tsquery('simple', $1)
|
||||||
@@ -125,6 +127,7 @@ func (r *Repository) Search(ctx context.Context, query string, limit int) ([]*Di
|
|||||||
OR dt.name ILIKE '%' || $1 || '%'
|
OR dt.name ILIKE '%' || $1 || '%'
|
||||||
OR similarity(COALESCE(dt.name, d.name), $1) > 0.3
|
OR similarity(COALESCE(dt.name, d.name), $1) > 0.3
|
||||||
)
|
)
|
||||||
|
GROUP BY d.id, dt.name, d.name, d.image_url, d.avg_rating
|
||||||
ORDER BY rank DESC
|
ORDER BY rank DESC
|
||||||
LIMIT $2`
|
LIMIT $2`
|
||||||
|
|
||||||
@@ -138,7 +141,7 @@ func (r *Repository) Search(ctx context.Context, query string, limit int) ([]*Di
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var result DishSearchResult
|
var result DishSearchResult
|
||||||
var rank float64
|
var rank float64
|
||||||
if scanError := rows.Scan(&result.ID, &result.Name, &result.ImageURL, &result.AvgRating, &rank); scanError != nil {
|
if scanError := rows.Scan(&result.ID, &result.Name, &result.ImageURL, &result.AvgRating, &result.CaloriesPerServing, &rank); scanError != nil {
|
||||||
return nil, fmt.Errorf("scan dish search result: %w", scanError)
|
return nil, fmt.Errorf("scan dish search result: %w", scanError)
|
||||||
}
|
}
|
||||||
results = append(results, &result)
|
results = append(results, &result)
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ class DishSearchResult {
|
|||||||
final String name;
|
final String name;
|
||||||
final String? imageUrl;
|
final String? imageUrl;
|
||||||
final double avgRating;
|
final double avgRating;
|
||||||
|
final double? caloriesPerServing;
|
||||||
|
|
||||||
const DishSearchResult({
|
const DishSearchResult({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
this.imageUrl,
|
this.imageUrl,
|
||||||
required this.avgRating,
|
required this.avgRating,
|
||||||
|
this.caloriesPerServing,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory DishSearchResult.fromJson(Map<String, dynamic> json) {
|
factory DishSearchResult.fromJson(Map<String, dynamic> json) {
|
||||||
@@ -21,6 +23,8 @@ class DishSearchResult {
|
|||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
imageUrl: json['image_url'] as String?,
|
imageUrl: json['image_url'] as String?,
|
||||||
avgRating: (json['avg_rating'] as num?)?.toDouble() ?? 0,
|
avgRating: (json['avg_rating'] as num?)?.toDouble() ?? 0,
|
||||||
|
caloriesPerServing:
|
||||||
|
(json['calories_per_serving'] as num?)?.toDouble(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -484,7 +484,9 @@ class _FoodTile extends StatelessWidget {
|
|||||||
size: 20, color: Colors.green),
|
size: 20, color: Colors.green),
|
||||||
),
|
),
|
||||||
title: dish.name,
|
title: dish.name,
|
||||||
subtitle: null,
|
subtitle: dish.caloriesPerServing != null
|
||||||
|
? '${dish.caloriesPerServing!.toInt()} kcal / serving'
|
||||||
|
: null,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -635,12 +637,11 @@ class _DishPortionSheetState extends ConsumerState<_DishPortionSheet> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Quick-select portion buttons
|
// Quick-select portion buttons
|
||||||
Row(
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
for (final quickValue in [0.5, 1.0, 1.5, 2.0])
|
for (final quickValue in [0.5, 1.0, 1.5, 2.0])
|
||||||
Padding(
|
OutlinedButton(
|
||||||
padding: const EdgeInsets.only(right: 8),
|
|
||||||
child: OutlinedButton(
|
|
||||||
onPressed: () => _setPortions(quickValue),
|
onPressed: () => _setPortions(quickValue),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
@@ -656,7 +657,6 @@ class _DishPortionSheetState extends ConsumerState<_DishPortionSheet> {
|
|||||||
? quickValue.toInt().toString()
|
? quickValue.toInt().toString()
|
||||||
: quickValue.toStringAsFixed(1)),
|
: quickValue.toStringAsFixed(1)),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|||||||
Reference in New Issue
Block a user