Initial commit: Sage Kitchen Management App v1.0.0
✨ Features implemented: - Smart inventory tracking with Hive database - Barcode scanning with auto-populated product info - Multiple API fallbacks (Open Food Facts, UPCItemDB) - Smart expiration date predictions by category - Discord webhook notifications (persisted) - Custom sage leaf vector icon - Material Design 3 UI with sage green theme - Privacy Policy & Terms of Service - Local-first, privacy-focused architecture 🎨 UI/UX: - Home dashboard with inventory stats - Add Item screen with barcode integration - Inventory list with expiration indicators - Settings with persistent preferences - About section with legal docs 🔧 Technical: - Flutter 3.35.5 with Riverpod state management - Hive 2.2.3 for local database - Mobile scanner for barcode detection - Feature-first architecture 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
178
lib/features/inventory/models/food_item.dart
Normal file
178
lib/features/inventory/models/food_item.dart
Normal file
@@ -0,0 +1,178 @@
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'food_item.g.dart';
|
||||
|
||||
/// Represents a food item in the inventory
|
||||
@HiveType(typeId: 0)
|
||||
class FoodItem extends HiveObject {
|
||||
// Basic Info
|
||||
@HiveField(0)
|
||||
late String name;
|
||||
|
||||
@HiveField(1)
|
||||
String? barcode;
|
||||
|
||||
@HiveField(2)
|
||||
late int quantity;
|
||||
|
||||
@HiveField(3)
|
||||
String? unit; // "bottles", "lbs", "oz", "items"
|
||||
|
||||
// Dates
|
||||
@HiveField(4)
|
||||
late DateTime purchaseDate;
|
||||
|
||||
@HiveField(5)
|
||||
late DateTime expirationDate;
|
||||
|
||||
// Organization
|
||||
@HiveField(6)
|
||||
late int locationIndex; // Store as int for Hive
|
||||
|
||||
@HiveField(7)
|
||||
String? category; // Auto from barcode or manual
|
||||
|
||||
// Media & Notes
|
||||
@HiveField(8)
|
||||
String? photoUrl; // Cached from API or user uploaded
|
||||
|
||||
@HiveField(9)
|
||||
String? notes;
|
||||
|
||||
// Multi-user support (for future phases)
|
||||
@HiveField(10)
|
||||
String? userId;
|
||||
|
||||
@HiveField(11)
|
||||
String? householdId;
|
||||
|
||||
// Sync tracking
|
||||
@HiveField(12)
|
||||
DateTime? lastModified;
|
||||
|
||||
@HiveField(13)
|
||||
bool syncedToCloud = false;
|
||||
|
||||
// Computed properties
|
||||
Location get location => Location.values[locationIndex];
|
||||
set location(Location loc) => locationIndex = loc.index;
|
||||
|
||||
int get daysUntilExpiration {
|
||||
return expirationDate.difference(DateTime.now()).inDays;
|
||||
}
|
||||
|
||||
ExpirationStatus get expirationStatus {
|
||||
final days = daysUntilExpiration;
|
||||
if (days < 0) return ExpirationStatus.expired;
|
||||
if (days <= 3) return ExpirationStatus.critical;
|
||||
if (days <= 7) return ExpirationStatus.warning;
|
||||
if (days <= 14) return ExpirationStatus.caution;
|
||||
return ExpirationStatus.fresh;
|
||||
}
|
||||
|
||||
bool get isExpired => daysUntilExpiration < 0;
|
||||
|
||||
bool get isExpiringSoon => daysUntilExpiration <= 7 && daysUntilExpiration >= 0;
|
||||
}
|
||||
|
||||
/// Location where food is stored
|
||||
@HiveType(typeId: 1)
|
||||
enum Location {
|
||||
@HiveField(0)
|
||||
fridge,
|
||||
@HiveField(1)
|
||||
freezer,
|
||||
@HiveField(2)
|
||||
pantry,
|
||||
@HiveField(3)
|
||||
spiceRack,
|
||||
@HiveField(4)
|
||||
countertop,
|
||||
@HiveField(5)
|
||||
other,
|
||||
}
|
||||
|
||||
/// Expiration status based on days until expiration
|
||||
@HiveType(typeId: 2)
|
||||
enum ExpirationStatus {
|
||||
@HiveField(0)
|
||||
fresh, // > 14 days
|
||||
@HiveField(1)
|
||||
caution, // 8-14 days
|
||||
@HiveField(2)
|
||||
warning, // 4-7 days
|
||||
@HiveField(3)
|
||||
critical, // 1-3 days
|
||||
@HiveField(4)
|
||||
expired, // 0 or negative days
|
||||
}
|
||||
|
||||
// Extension to get user-friendly names for Location
|
||||
extension LocationExtension on Location {
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case Location.fridge:
|
||||
return 'Fridge';
|
||||
case Location.freezer:
|
||||
return 'Freezer';
|
||||
case Location.pantry:
|
||||
return 'Pantry';
|
||||
case Location.spiceRack:
|
||||
return 'Spice Rack';
|
||||
case Location.countertop:
|
||||
return 'Countertop';
|
||||
case Location.other:
|
||||
return 'Other';
|
||||
}
|
||||
}
|
||||
|
||||
String get emoji {
|
||||
switch (this) {
|
||||
case Location.fridge:
|
||||
return '🧊';
|
||||
case Location.freezer:
|
||||
return '❄️';
|
||||
case Location.pantry:
|
||||
return '🗄️';
|
||||
case Location.spiceRack:
|
||||
return '🧂';
|
||||
case Location.countertop:
|
||||
return '🪴';
|
||||
case Location.other:
|
||||
return '📦';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extension for ExpirationStatus
|
||||
extension ExpirationStatusExtension on ExpirationStatus {
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case ExpirationStatus.fresh:
|
||||
return 'Fresh';
|
||||
case ExpirationStatus.caution:
|
||||
return 'Use within 2 weeks';
|
||||
case ExpirationStatus.warning:
|
||||
return 'Use soon';
|
||||
case ExpirationStatus.critical:
|
||||
return 'Use now!';
|
||||
case ExpirationStatus.expired:
|
||||
return 'Expired';
|
||||
}
|
||||
}
|
||||
|
||||
int get colorValue {
|
||||
switch (this) {
|
||||
case ExpirationStatus.fresh:
|
||||
return 0xFF4CAF50; // Green
|
||||
case ExpirationStatus.caution:
|
||||
return 0xFFFFEB3B; // Yellow
|
||||
case ExpirationStatus.warning:
|
||||
return 0xFFFF9800; // Orange
|
||||
case ExpirationStatus.critical:
|
||||
return 0xFFF44336; // Red
|
||||
case ExpirationStatus.expired:
|
||||
return 0xFF9E9E9E; // Gray
|
||||
}
|
||||
}
|
||||
}
|
192
lib/features/inventory/models/food_item.g.dart
Normal file
192
lib/features/inventory/models/food_item.g.dart
Normal file
@@ -0,0 +1,192 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'food_item.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class FoodItemAdapter extends TypeAdapter<FoodItem> {
|
||||
@override
|
||||
final int typeId = 0;
|
||||
|
||||
@override
|
||||
FoodItem read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return FoodItem()
|
||||
..name = fields[0] as String
|
||||
..barcode = fields[1] as String?
|
||||
..quantity = fields[2] as int
|
||||
..unit = fields[3] as String?
|
||||
..purchaseDate = fields[4] as DateTime
|
||||
..expirationDate = fields[5] as DateTime
|
||||
..locationIndex = fields[6] as int
|
||||
..category = fields[7] as String?
|
||||
..photoUrl = fields[8] as String?
|
||||
..notes = fields[9] as String?
|
||||
..userId = fields[10] as String?
|
||||
..householdId = fields[11] as String?
|
||||
..lastModified = fields[12] as DateTime?
|
||||
..syncedToCloud = fields[13] as bool;
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, FoodItem obj) {
|
||||
writer
|
||||
..writeByte(14)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
..write(obj.barcode)
|
||||
..writeByte(2)
|
||||
..write(obj.quantity)
|
||||
..writeByte(3)
|
||||
..write(obj.unit)
|
||||
..writeByte(4)
|
||||
..write(obj.purchaseDate)
|
||||
..writeByte(5)
|
||||
..write(obj.expirationDate)
|
||||
..writeByte(6)
|
||||
..write(obj.locationIndex)
|
||||
..writeByte(7)
|
||||
..write(obj.category)
|
||||
..writeByte(8)
|
||||
..write(obj.photoUrl)
|
||||
..writeByte(9)
|
||||
..write(obj.notes)
|
||||
..writeByte(10)
|
||||
..write(obj.userId)
|
||||
..writeByte(11)
|
||||
..write(obj.householdId)
|
||||
..writeByte(12)
|
||||
..write(obj.lastModified)
|
||||
..writeByte(13)
|
||||
..write(obj.syncedToCloud);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is FoodItemAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class LocationAdapter extends TypeAdapter<Location> {
|
||||
@override
|
||||
final int typeId = 1;
|
||||
|
||||
@override
|
||||
Location read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 0:
|
||||
return Location.fridge;
|
||||
case 1:
|
||||
return Location.freezer;
|
||||
case 2:
|
||||
return Location.pantry;
|
||||
case 3:
|
||||
return Location.spiceRack;
|
||||
case 4:
|
||||
return Location.countertop;
|
||||
case 5:
|
||||
return Location.other;
|
||||
default:
|
||||
return Location.fridge;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, Location obj) {
|
||||
switch (obj) {
|
||||
case Location.fridge:
|
||||
writer.writeByte(0);
|
||||
break;
|
||||
case Location.freezer:
|
||||
writer.writeByte(1);
|
||||
break;
|
||||
case Location.pantry:
|
||||
writer.writeByte(2);
|
||||
break;
|
||||
case Location.spiceRack:
|
||||
writer.writeByte(3);
|
||||
break;
|
||||
case Location.countertop:
|
||||
writer.writeByte(4);
|
||||
break;
|
||||
case Location.other:
|
||||
writer.writeByte(5);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is LocationAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class ExpirationStatusAdapter extends TypeAdapter<ExpirationStatus> {
|
||||
@override
|
||||
final int typeId = 2;
|
||||
|
||||
@override
|
||||
ExpirationStatus read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 0:
|
||||
return ExpirationStatus.fresh;
|
||||
case 1:
|
||||
return ExpirationStatus.caution;
|
||||
case 2:
|
||||
return ExpirationStatus.warning;
|
||||
case 3:
|
||||
return ExpirationStatus.critical;
|
||||
case 4:
|
||||
return ExpirationStatus.expired;
|
||||
default:
|
||||
return ExpirationStatus.fresh;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ExpirationStatus obj) {
|
||||
switch (obj) {
|
||||
case ExpirationStatus.fresh:
|
||||
writer.writeByte(0);
|
||||
break;
|
||||
case ExpirationStatus.caution:
|
||||
writer.writeByte(1);
|
||||
break;
|
||||
case ExpirationStatus.warning:
|
||||
writer.writeByte(2);
|
||||
break;
|
||||
case ExpirationStatus.critical:
|
||||
writer.writeByte(3);
|
||||
break;
|
||||
case ExpirationStatus.expired:
|
||||
writer.writeByte(4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ExpirationStatusAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
Reference in New Issue
Block a user