feat: rename ingredients→products, products→user_products; add barcode/OFF import

- Rename catalog: ingredient/* → product/* (canonical_name, barcode, nutrition per 100g)
- Rename pantry: product/* → userproduct/* (user-owned items with expiry)
- Squash migrations into single 001_initial_schema.sql (clean-db baseline)
- product_categories: add English canonical name column; fix COALESCE in queries
- Remove product_translations: product names are stored in their original language
- Add default_unit_name to product API responses via unit_translations JOIN
- Add cmd/importoff: bulk import from OpenFoodFacts JSONL dump (COPY + ON CONFLICT)
- Diary: support product_id entries alongside dish_id (CHECK num_nonnulls = 1)
- Home: getLoggedCalories joins both recipes and catalog products
- Flutter: rename models/providers/services to match backend rename
- Flutter: add barcode scan flow for diary (mobile_scanner, product_portion_sheet)
- Flutter: localise 6 new keys across 12 languages (barcode scan, portion weight)
- Routes: GET /products/search, GET /products/barcode/{barcode}, /user-products

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dbastrikin
2026-03-21 12:45:48 +02:00
parent 6861e5e754
commit 205edbdade
72 changed files with 2588 additions and 1444 deletions

View File

@@ -2,45 +2,48 @@ import 'package:json_annotation/json_annotation.dart';
part 'product.g.dart';
/// Catalog product (shared nutrition database entry).
@JsonSerializable()
class Product {
class CatalogProduct {
final String id;
@JsonKey(name: 'user_id')
final String userId;
@JsonKey(name: 'mapping_id')
final String? mappingId;
final String name;
final double quantity;
final String unit;
@JsonKey(name: 'canonical_name')
final String canonicalName;
@JsonKey(name: 'category_name')
final String? categoryName;
final String? category;
@JsonKey(name: 'default_unit')
final String? defaultUnit;
@JsonKey(name: 'storage_days')
final int storageDays;
@JsonKey(name: 'added_at')
final DateTime addedAt;
@JsonKey(name: 'expires_at')
final DateTime expiresAt;
@JsonKey(name: 'days_left')
final int daysLeft;
@JsonKey(name: 'expiring_soon')
final bool expiringSoon;
final int? storageDays;
final String? barcode;
@JsonKey(name: 'calories_per_100g')
final double? caloriesPer100g;
@JsonKey(name: 'protein_per_100g')
final double? proteinPer100g;
@JsonKey(name: 'fat_per_100g')
final double? fatPer100g;
@JsonKey(name: 'carbs_per_100g')
final double? carbsPer100g;
const Product({
const CatalogProduct({
required this.id,
required this.userId,
this.mappingId,
required this.name,
required this.quantity,
required this.unit,
required this.canonicalName,
this.categoryName,
this.category,
required this.storageDays,
required this.addedAt,
required this.expiresAt,
required this.daysLeft,
required this.expiringSoon,
this.defaultUnit,
this.storageDays,
this.barcode,
this.caloriesPer100g,
this.proteinPer100g,
this.fatPer100g,
this.carbsPer100g,
});
factory Product.fromJson(Map<String, dynamic> json) =>
_$ProductFromJson(json);
/// Display name is the server-resolved canonical name (language-aware from backend).
String get displayName => canonicalName;
Map<String, dynamic> toJson() => _$ProductToJson(this);
factory CatalogProduct.fromJson(Map<String, dynamic> json) =>
_$CatalogProductFromJson(json);
Map<String, dynamic> toJson() => _$CatalogProductToJson(this);
}