✨ 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>
1728 lines
42 KiB
Markdown
1728 lines
42 KiB
Markdown
# SAGE - COMPLETE PROJECT DOCUMENTATION
|
|
## Smart Kitchen Management System 🌿
|
|
|
|
**Version:** 1.0.0
|
|
**Created:** October 3, 2025
|
|
**Platform:** Flutter (Android & iOS)
|
|
**Status:** Active Development 🔥
|
|
|
|
---
|
|
|
|
## 📖 TABLE OF CONTENTS
|
|
|
|
1. [Project Overview](#project-overview)
|
|
2. [Core Features](#core-features)
|
|
3. [Technical Architecture](#technical-architecture)
|
|
4. [Data Models](#data-models)
|
|
5. [API Integrations](#api-integrations)
|
|
6. [User Flows](#user-flows)
|
|
7. [Notification System](#notification-system)
|
|
8. [Multi-User & Sync](#multi-user--sync)
|
|
9. [Security & Privacy](#security--privacy)
|
|
10. [Testing Strategy](#testing-strategy)
|
|
11. [Deployment](#deployment)
|
|
|
|
---
|
|
|
|
## 📱 PROJECT OVERVIEW
|
|
|
|
### The Problem
|
|
- People forget what's in their fridge/pantry
|
|
- Food expires and gets wasted → money wasted
|
|
- Constant grocery store trips forgetting items
|
|
- End up ordering takeout when food at home expires
|
|
- No organization = kitchen chaos
|
|
|
|
### The Solution: SAGE
|
|
A comprehensive kitchen management app that:
|
|
1. **Tracks** all your food inventory with expiration dates
|
|
2. **Alerts** you before food expires (with Discord integration!)
|
|
3. **Manages** your recipes and connects them to inventory
|
|
4. **Generates** smart shopping lists
|
|
5. **Shares** with household members
|
|
6. **Works offline-first** with optional cloud sync
|
|
|
|
### Why "Sage"?
|
|
- 🌿 The herb (kitchen theme)
|
|
- 🧠 Wisdom and smart decisions
|
|
- 💚 Simple, clean, memorable
|
|
|
|
### Target Users
|
|
- Busy individuals who want to save money
|
|
- Families managing shared kitchens
|
|
- Anyone tired of wasting food
|
|
- People who hate grocery shopping inefficiency
|
|
- Budget-conscious cooks
|
|
|
|
---
|
|
|
|
## 🎯 CORE FEATURES
|
|
|
|
### 1. INVENTORY TRACKER 📦
|
|
|
|
**The Foundation:** Track everything in your kitchen
|
|
|
|
**Key Capabilities:**
|
|
- Add items via barcode scan OR manual entry
|
|
- Auto-populate from Open Food Facts (2M+ products!)
|
|
- Track quantity, expiration date, location
|
|
- Organize by location (fridge, freezer, pantry, etc.)
|
|
- Categorize automatically or manually
|
|
- Attach photos (cached locally)
|
|
- Search and filter inventory
|
|
- Quick edit/delete items
|
|
|
|
**Barcode Scanning:**
|
|
```
|
|
User scans barcode
|
|
↓
|
|
Query Open Food Facts API
|
|
↓
|
|
Product found?
|
|
├─ YES → Auto-fill: name, photo, category, typical shelf life
|
|
│ User adds: quantity, actual expiration, location
|
|
│ → Save to local database
|
|
│
|
|
└─ NO → Manual entry form
|
|
User fills everything
|
|
→ Save to local database
|
|
```
|
|
|
|
**Data Tracked Per Item:**
|
|
- Name
|
|
- Barcode (if scanned)
|
|
- Quantity & unit
|
|
- Purchase date
|
|
- Expiration date
|
|
- Location (fridge/freezer/pantry/other)
|
|
- Category (auto or manual)
|
|
- Photo URL (cached)
|
|
- Notes (optional)
|
|
- Last modified timestamp
|
|
- Sync status
|
|
|
|
---
|
|
|
|
### 2. SMART ALERTS 🔔
|
|
|
|
**The Brain:** Never let food go to waste
|
|
|
|
**Alert Timeline:**
|
|
|
|
| Time Before Expiration | Alert Type | Message |
|
|
|------------------------|------------|---------|
|
|
| 1 month | FYI notification | "Ranch dressing expires in 30 days" |
|
|
| 2 weeks | Heads up | "Sour cream expires in 14 days" |
|
|
| 1 week | Use soon! | "Milk expires in 7 days - Tap for recipes" |
|
|
| Before next shopping day | Shopping alert | "3 items expire before next week!" |
|
|
|
|
**Shopping Day Intelligence:**
|
|
```
|
|
User sets shopping frequency:
|
|
- Weekly (every Tuesday)
|
|
- Bi-weekly (every other Monday)
|
|
- Monthly (first Saturday)
|
|
- Pay cycle (15th and 30th)
|
|
- Custom days
|
|
|
|
System calculates next shopping day
|
|
↓
|
|
For each item expiring before next shopping day:
|
|
→ Send notification
|
|
→ Add to "Add to Shopping List" quick action
|
|
→ Send to Discord channel (if enabled)
|
|
```
|
|
|
|
**Notification Types:**
|
|
1. **Local Push Notifications**
|
|
- Scheduled in advance
|
|
- Tappable (opens relevant screen)
|
|
- Customizable (can disable certain types)
|
|
|
|
2. **Discord Webhooks**
|
|
- Critical alerts only (shopping day, urgent expirations)
|
|
- Formatted with emojis and priority
|
|
- @everyone tag for household
|
|
|
|
**Example Discord Message:**
|
|
```markdown
|
|
🌿 **Sage Alert** 🌿
|
|
@everyone Shopping day is tomorrow!
|
|
|
|
**Expiring before next week:**
|
|
🔴 Ranch dressing (expires Oct 10) - 3 days
|
|
🔴 Croutons (expires Oct 12) - 5 days
|
|
🟡 Milk (expires Oct 15) - 8 days
|
|
|
|
**Suggested Actions:**
|
|
• Add to shopping list
|
|
• Check "Use It Up" recipes
|
|
• Update quantities if already replaced
|
|
|
|
Don't forget to restock! 🛒
|
|
```
|
|
|
|
---
|
|
|
|
### 3. RECIPE BOOK 📖
|
|
|
|
**The Inspiration:** Never wonder "what's for dinner?" again
|
|
|
|
**Core Functions:**
|
|
- Add recipes manually or copy/paste from websites
|
|
- Store ingredients with quantities
|
|
- Write/paste instructions
|
|
- Add optional photos
|
|
- Tag recipes (Quick, Vegetarian, Fancy, etc.)
|
|
- Share recipes with community (optional)
|
|
- Mark favorites
|
|
|
|
**Smart Features:**
|
|
- **"What Can I Make?"** - Shows recipes you can make with current inventory
|
|
- Displays % of ingredients you have
|
|
- Sorts by completeness
|
|
- Shows what's missing
|
|
|
|
- **"Use It Up!"** - Suggests recipes using expiring items
|
|
- Prioritizes items expiring soonest
|
|
- Helps prevent waste
|
|
|
|
**Recipe Data Structure:**
|
|
```dart
|
|
Recipe {
|
|
name: "Caesar Salad"
|
|
ingredients: [
|
|
{name: "Romaine lettuce", quantity: 1, unit: "head"},
|
|
{name: "Caesar dressing", quantity: 0.5, unit: "cup"},
|
|
{name: "Croutons", quantity: 1, unit: "cup"},
|
|
{name: "Parmesan", quantity: 0.25, unit: "cup", optional: true}
|
|
]
|
|
instructions: "Chop lettuce. Toss with dressing. Top with croutons and parmesan."
|
|
prepTime: 10 minutes
|
|
servings: 2
|
|
tags: ["Quick", "Salad", "Vegetarian"]
|
|
photo: "url_or_local_path"
|
|
isPublic: true // Share with community
|
|
createdBy: user_id
|
|
}
|
|
```
|
|
|
|
**Copy/Paste from Websites:**
|
|
User pastes URL or text block → Parser extracts:
|
|
- Recipe title
|
|
- Ingredient list (with quantities)
|
|
- Instructions
|
|
- Optional: photo, prep time, servings
|
|
|
|
Common formats supported:
|
|
- Recipe schema (schema.org/Recipe)
|
|
- Plain text with smart parsing
|
|
- Popular recipe sites (AllRecipes, Food Network, etc.)
|
|
|
|
---
|
|
|
|
### 4. SHOPPING LISTS 🛒
|
|
|
|
**The Planner:** Efficient grocery trips, every time
|
|
|
|
**Key Features:**
|
|
- **Multiple Lists** - Separate lists for different stores
|
|
- "Costco"
|
|
- "Trader Joe's"
|
|
- "Farmers Market"
|
|
- Custom names
|
|
|
|
- **Smart List Building**
|
|
- Add items manually
|
|
- One-click add from recipes
|
|
- Suggested items based on expiring inventory
|
|
|
|
- **While Shopping**
|
|
- Check off items as you shop
|
|
- Uncheck if needed
|
|
- Sort by store section (optional)
|
|
- Quick quantity adjust
|
|
|
|
- **After Shopping**
|
|
- "Add All to Inventory" quick action
|
|
- Batch-add checked items
|
|
- Auto-populate likely expiration dates
|
|
- Clear completed items
|
|
|
|
**Sharing:**
|
|
- Share lists with household members
|
|
- Real-time updates (if cloud sync enabled)
|
|
- See who added what
|
|
- Collaborative shopping
|
|
|
|
**List Organization:**
|
|
```
|
|
Shopping List: "Costco"
|
|
Items:
|
|
☐ Milk (2 gallons) - Added from: Ranch expires alert
|
|
☑ Chicken breast (2 lbs) - Added from: Recipe "Chicken Tacos"
|
|
☐ Romaine lettuce (2 heads) - Added manually
|
|
☐ Croutons (1 bag) - Added from: Recipe "Caesar Salad"
|
|
|
|
Quick Actions:
|
|
- Add recipe ingredients
|
|
- Add expiring items
|
|
- Share list
|
|
- Sort by aisle
|
|
```
|
|
|
|
---
|
|
|
|
### 5. MULTI-USER & SYNC ☁️
|
|
|
|
**The Connector:** Share with your household
|
|
|
|
**Three Sync Modes (User's Choice):**
|
|
|
|
#### Mode 1: Cloud Sync (Supabase) ☁️
|
|
**Best for:** Most users, easy setup, reliable
|
|
|
|
**Features:**
|
|
- Real-time sync across devices
|
|
- User authentication
|
|
- Household groups (shared inventory/lists)
|
|
- Recipe community sharing
|
|
- Automatic backups
|
|
|
|
**Technical:**
|
|
- PostgreSQL database (Supabase)
|
|
- Row-level security
|
|
- Real-time subscriptions
|
|
- Free tier: 500MB DB, 2GB bandwidth/month
|
|
|
|
**User Flow:**
|
|
```
|
|
Sign up with email
|
|
↓
|
|
Create or join household
|
|
↓
|
|
All devices sync automatically
|
|
↓
|
|
Changes propagate in real-time
|
|
```
|
|
|
|
#### Mode 2: Completely Free (Self-Hosted) 🏠
|
|
**Best for:** Privacy-conscious users, no cloud dependency
|
|
|
|
**Options:**
|
|
|
|
**A. Export/Import**
|
|
- Export inventory/recipes to JSON
|
|
- Share file via any method (email, cloud storage, etc.)
|
|
- Import on other device
|
|
- Manual sync process
|
|
|
|
**B. Local WiFi Sync**
|
|
- Devices on same network
|
|
- Peer-to-peer sync (no internet needed!)
|
|
- Automatic discovery
|
|
- One device = "host"
|
|
|
|
**C. Self-Host Backend**
|
|
- Docker Compose setup (we provide!)
|
|
- Run on Raspberry Pi or home server
|
|
- Full control of data
|
|
- Zero recurring costs
|
|
|
|
**Self-Hosted Setup:**
|
|
```bash
|
|
# We provide this in docs!
|
|
git clone https://github.com/sage-app/backend
|
|
cd backend
|
|
docker-compose up -d
|
|
|
|
# Configure app to point to your server
|
|
# http://192.168.1.100:3000
|
|
```
|
|
|
|
#### Mode 3: Local Only 📱
|
|
**Best for:** Solo users, maximum privacy, no sharing needed
|
|
|
|
**Features:**
|
|
- Everything on device
|
|
- Zero external dependencies
|
|
- Export for backup
|
|
- Fast and simple
|
|
|
|
**Offline-First Philosophy (ALL MODES):**
|
|
```
|
|
Every action saves locally FIRST
|
|
↓
|
|
If online & sync enabled:
|
|
Queue for sync
|
|
↓
|
|
Sync when possible
|
|
↓
|
|
Update local cache
|
|
|
|
App works PERFECTLY offline
|
|
Never wait for network
|
|
Graceful sync when available
|
|
```
|
|
|
|
**Conflict Resolution:**
|
|
- Last-write-wins (timestamp-based)
|
|
- Show notification if conflict detected
|
|
- User can view both versions and choose
|
|
|
|
---
|
|
|
|
## 🏗️ TECHNICAL ARCHITECTURE
|
|
|
|
### Tech Stack Overview
|
|
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ FLUTTER APP (Dart) │
|
|
│ ┌───────────────────────────────┐ │
|
|
│ │ UI Layer (Widgets) │ │
|
|
│ └──────────┬────────────────────┘ │
|
|
│ │ │
|
|
│ ┌──────────▼────────────────────┐ │
|
|
│ │ State Management (Riverpod) │ │
|
|
│ └──────────┬────────────────────┘ │
|
|
│ │ │
|
|
│ ┌──────────▼────────────────────┐ │
|
|
│ │ Business Logic Layer │ │
|
|
│ │ - Controllers │ │
|
|
│ │ - Services │ │
|
|
│ │ - Repositories │ │
|
|
│ └──────────┬────────────────────┘ │
|
|
│ │ │
|
|
│ ┌──────────▼────────────────────┐ │
|
|
│ │ Data Layer │ │
|
|
│ │ ┌──────────┐ ┌────────────┐ │ │
|
|
│ │ │ Isar │ │ Supabase │ │ │
|
|
│ │ │ (Local) │ │ (Cloud) │ │ │
|
|
│ │ └──────────┘ └────────────┘ │ │
|
|
│ └───────────────────────────────┘ │
|
|
└─────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────┐
|
|
│ External APIs │
|
|
│ - Open Food Facts │
|
|
│ - Discord Webhook │
|
|
└────────────────────┘
|
|
```
|
|
|
|
### Key Technologies
|
|
|
|
| Component | Technology | Purpose |
|
|
|-----------|-----------|---------|
|
|
| Framework | Flutter 3.x | Cross-platform UI |
|
|
| Language | Dart 3.x | Type-safe, fast, AOT compiled |
|
|
| State Management | Riverpod 2.x | Reactive state, dependency injection |
|
|
| Local Database | Isar 3.x | Fast NoSQL, offline-first |
|
|
| Cloud Database | Supabase | PostgreSQL, real-time, auth |
|
|
| Barcode Scanner | mobile_scanner | Camera-based scanning |
|
|
| Notifications | flutter_local_notifications | Scheduled alerts |
|
|
| Image Caching | cached_network_image | Performance optimization |
|
|
| HTTP Client | http | API requests |
|
|
|
|
### Project Structure
|
|
|
|
```
|
|
sage/
|
|
├── lib/
|
|
│ ├── main.dart # App entry point
|
|
│ ├── app.dart # App widget, theme, routing
|
|
│ │
|
|
│ ├── core/ # Core utilities
|
|
│ │ ├── constants/
|
|
│ │ │ ├── app_constants.dart
|
|
│ │ │ ├── api_constants.dart
|
|
│ │ │ └── colors.dart
|
|
│ │ ├── utils/
|
|
│ │ │ ├── date_utils.dart
|
|
│ │ │ ├── validators.dart
|
|
│ │ │ └── formatters.dart
|
|
│ │ └── extensions/
|
|
│ │ ├── date_extensions.dart
|
|
│ │ └── string_extensions.dart
|
|
│ │
|
|
│ ├── features/ # Feature modules
|
|
│ │ ├── inventory/
|
|
│ │ │ ├── models/
|
|
│ │ │ │ └── food_item.dart
|
|
│ │ │ ├── repositories/
|
|
│ │ │ │ └── inventory_repository.dart
|
|
│ │ │ ├── controllers/
|
|
│ │ │ │ └── inventory_controller.dart
|
|
│ │ │ ├── screens/
|
|
│ │ │ │ ├── inventory_screen.dart
|
|
│ │ │ │ ├── add_item_screen.dart
|
|
│ │ │ │ └── item_detail_screen.dart
|
|
│ │ │ └── widgets/
|
|
│ │ │ ├── inventory_list.dart
|
|
│ │ │ ├── expiration_badge.dart
|
|
│ │ │ └── barcode_scanner.dart
|
|
│ │ │
|
|
│ │ ├── recipes/
|
|
│ │ │ ├── models/
|
|
│ │ │ │ └── recipe.dart
|
|
│ │ │ ├── repositories/
|
|
│ │ │ │ └── recipe_repository.dart
|
|
│ │ │ ├── controllers/
|
|
│ │ │ │ └── recipe_controller.dart
|
|
│ │ │ ├── screens/
|
|
│ │ │ │ ├── recipes_screen.dart
|
|
│ │ │ │ ├── add_recipe_screen.dart
|
|
│ │ │ │ ├── recipe_detail_screen.dart
|
|
│ │ │ │ └── what_can_i_make_screen.dart
|
|
│ │ │ └── widgets/
|
|
│ │ │ ├── recipe_card.dart
|
|
│ │ │ └── ingredient_list.dart
|
|
│ │ │
|
|
│ │ ├── shopping/
|
|
│ │ │ ├── models/
|
|
│ │ │ │ └── shopping_list.dart
|
|
│ │ │ ├── repositories/
|
|
│ │ │ │ └── shopping_repository.dart
|
|
│ │ │ ├── controllers/
|
|
│ │ │ │ └── shopping_controller.dart
|
|
│ │ │ ├── screens/
|
|
│ │ │ │ ├── shopping_lists_screen.dart
|
|
│ │ │ │ └── list_detail_screen.dart
|
|
│ │ │ └── widgets/
|
|
│ │ │ └── shopping_item.dart
|
|
│ │ │
|
|
│ │ ├── home/
|
|
│ │ │ ├── screens/
|
|
│ │ │ │ └── home_screen.dart
|
|
│ │ │ └── widgets/
|
|
│ │ │ ├── expiring_soon_carousel.dart
|
|
│ │ │ └── quick_stats.dart
|
|
│ │ │
|
|
│ │ ├── settings/
|
|
│ │ │ ├── models/
|
|
│ │ │ │ └── user_settings.dart
|
|
│ │ │ ├── repositories/
|
|
│ │ │ │ └── settings_repository.dart
|
|
│ │ │ ├── controllers/
|
|
│ │ │ │ └── settings_controller.dart
|
|
│ │ │ └── screens/
|
|
│ │ │ ├── settings_screen.dart
|
|
│ │ │ ├── notifications_settings.dart
|
|
│ │ │ ├── sync_settings.dart
|
|
│ │ │ └── discord_settings.dart
|
|
│ │ │
|
|
│ │ └── auth/
|
|
│ │ ├── controllers/
|
|
│ │ │ └── auth_controller.dart
|
|
│ │ └── screens/
|
|
│ │ ├── login_screen.dart
|
|
│ │ └── signup_screen.dart
|
|
│ │
|
|
│ ├── services/ # Business logic services
|
|
│ │ ├── notification_service.dart
|
|
│ │ ├── sync_service.dart
|
|
│ │ ├── barcode_service.dart
|
|
│ │ ├── discord_service.dart
|
|
│ │ ├── openfoodfacts_service.dart
|
|
│ │ └── expiration_tracker_service.dart
|
|
│ │
|
|
│ ├── data/ # Data layer
|
|
│ │ ├── local/
|
|
│ │ │ └── isar_database.dart
|
|
│ │ └── remote/
|
|
│ │ └── supabase_client.dart
|
|
│ │
|
|
│ └── shared/ # Shared widgets & components
|
|
│ ├── widgets/
|
|
│ │ ├── custom_button.dart
|
|
│ │ ├── custom_text_field.dart
|
|
│ │ └── loading_indicator.dart
|
|
│ └── navigation/
|
|
│ └── app_router.dart
|
|
│
|
|
├── assets/ # Static assets
|
|
│ ├── images/
|
|
│ ├── icons/
|
|
│ └── fonts/
|
|
│
|
|
├── test/ # Tests
|
|
│ ├── unit/
|
|
│ ├── widget/
|
|
│ └── integration/
|
|
│
|
|
├── android/ # Android-specific
|
|
├── ios/ # iOS-specific
|
|
├── pubspec.yaml # Dependencies
|
|
└── README.md
|
|
```
|
|
|
|
---
|
|
|
|
## 💾 DATA MODELS (DETAILED)
|
|
|
|
### FoodItem Model
|
|
|
|
```dart
|
|
import 'package:isar/isar.dart';
|
|
|
|
part 'food_item.g.dart';
|
|
|
|
@collection
|
|
class FoodItem {
|
|
Id id = Isar.autoIncrement;
|
|
|
|
// Basic Info
|
|
late String name;
|
|
String? barcode;
|
|
late int quantity;
|
|
String? unit; // "bottles", "lbs", "oz", "items"
|
|
|
|
// Dates
|
|
late DateTime purchaseDate;
|
|
late DateTime expirationDate;
|
|
|
|
// Organization
|
|
@enumerated
|
|
late Location location;
|
|
String? category; // Auto from barcode or manual
|
|
|
|
// Media & Notes
|
|
String? photoUrl; // Cached from API or user uploaded
|
|
String? notes;
|
|
|
|
// Multi-user support
|
|
String? userId;
|
|
String? householdId;
|
|
|
|
// Sync tracking
|
|
DateTime? lastModified;
|
|
bool syncedToCloud = false;
|
|
|
|
// Computed properties (not stored)
|
|
@ignore
|
|
int get daysUntilExpiration {
|
|
return expirationDate.difference(DateTime.now()).inDays;
|
|
}
|
|
|
|
@ignore
|
|
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;
|
|
}
|
|
}
|
|
|
|
enum Location {
|
|
fridge,
|
|
freezer,
|
|
pantry,
|
|
spiceRack,
|
|
countertop,
|
|
other
|
|
}
|
|
|
|
enum ExpirationStatus {
|
|
fresh, // > 14 days
|
|
caution, // 8-14 days
|
|
warning, // 4-7 days
|
|
critical, // 1-3 days
|
|
expired // 0 or negative days
|
|
}
|
|
```
|
|
|
|
### Recipe Model
|
|
|
|
```dart
|
|
import 'package:isar/isar.dart';
|
|
|
|
part 'recipe.g.dart';
|
|
|
|
@collection
|
|
class Recipe {
|
|
Id id = Isar.autoIncrement;
|
|
|
|
// Basic Info
|
|
late String name;
|
|
late String instructions;
|
|
|
|
// Ingredients (embedded objects)
|
|
late List<Ingredient> ingredients;
|
|
|
|
// Media
|
|
String? photoUrl;
|
|
|
|
// Metadata
|
|
List<String> tags = [];
|
|
int? prepTimeMinutes;
|
|
int? cookTimeMinutes;
|
|
int? servings;
|
|
|
|
@enumerated
|
|
DifficultyLevel? difficulty;
|
|
|
|
// Sharing
|
|
String? createdBy; // user_id
|
|
bool isPublic = false;
|
|
bool isFavorite = false;
|
|
|
|
// Timestamps
|
|
DateTime? created;
|
|
DateTime? lastModified;
|
|
|
|
// Sync
|
|
bool syncedToCloud = false;
|
|
|
|
// Computed
|
|
@ignore
|
|
int get totalTimeMinutes {
|
|
return (prepTimeMinutes ?? 0) + (cookTimeMinutes ?? 0);
|
|
}
|
|
}
|
|
|
|
@embedded
|
|
class Ingredient {
|
|
late String name;
|
|
double? quantity;
|
|
String? unit;
|
|
bool optional = false;
|
|
|
|
// For matching against inventory
|
|
String? matchedItemId; // Link to FoodItem
|
|
}
|
|
|
|
enum DifficultyLevel {
|
|
easy,
|
|
medium,
|
|
hard
|
|
}
|
|
```
|
|
|
|
### ShoppingList Model
|
|
|
|
```dart
|
|
import 'package:isar/isar.dart';
|
|
|
|
part 'shopping_list.g.dart';
|
|
|
|
@collection
|
|
class ShoppingList {
|
|
Id id = Isar.autoIncrement;
|
|
|
|
late String name; // "Costco", "Trader Joe's"
|
|
|
|
List<ShoppingItem> items = [];
|
|
|
|
// Sharing
|
|
String? householdId;
|
|
List<String> sharedWith = [];
|
|
|
|
// Metadata
|
|
DateTime? created;
|
|
DateTime? lastModified;
|
|
|
|
// Sync
|
|
bool syncedToCloud = false;
|
|
|
|
// Computed
|
|
@ignore
|
|
int get totalItems => items.length;
|
|
|
|
@ignore
|
|
int get checkedItems => items.where((i) => i.checked).length;
|
|
|
|
@ignore
|
|
double get completionPercentage {
|
|
if (totalItems == 0) return 0;
|
|
return (checkedItems / totalItems) * 100;
|
|
}
|
|
}
|
|
|
|
@embedded
|
|
class ShoppingItem {
|
|
late String name;
|
|
double? quantity;
|
|
String? unit;
|
|
bool checked = false;
|
|
|
|
// Metadata
|
|
@enumerated
|
|
Priority priority = Priority.normal;
|
|
|
|
String? addedFrom; // "recipe:123", "inventory:456", "manual"
|
|
String? addedBy; // user_id
|
|
DateTime? addedAt;
|
|
|
|
String? notes;
|
|
}
|
|
|
|
enum Priority {
|
|
low,
|
|
normal,
|
|
high
|
|
}
|
|
```
|
|
|
|
### UserSettings Model
|
|
|
|
```dart
|
|
import 'package:isar/isar.dart';
|
|
|
|
part 'user_settings.g.dart';
|
|
|
|
@collection
|
|
class UserSettings {
|
|
Id id = Isar.autoIncrement;
|
|
|
|
// Shopping Schedule
|
|
@enumerated
|
|
ShoppingFrequency frequency = ShoppingFrequency.weekly;
|
|
|
|
// For weekly/biweekly
|
|
int? dayOfWeek; // 1-7 (Monday-Sunday)
|
|
|
|
// For monthly
|
|
int? dayOfMonth; // 1-31
|
|
|
|
// For pay cycle
|
|
List<int>? payCycleDays; // [15, 30]
|
|
|
|
// For custom
|
|
List<int>? customDays; // [1, 4, 6] = Mon, Thu, Sat
|
|
|
|
// Calculated
|
|
DateTime? nextShoppingDay;
|
|
|
|
// Notifications
|
|
bool enableNotifications = true;
|
|
bool notify1Month = true;
|
|
bool notify2Weeks = true;
|
|
bool notify1Week = true;
|
|
bool notifyShoppingDay = true;
|
|
|
|
// Discord
|
|
bool enableDiscord = false;
|
|
String? discordWebhookUrl;
|
|
bool discordCriticalOnly = true;
|
|
|
|
// Sync
|
|
@enumerated
|
|
SyncMode syncMode = SyncMode.localOnly;
|
|
|
|
String? supabaseUserId;
|
|
String? householdId;
|
|
|
|
// Display
|
|
bool showPhotos = true;
|
|
bool showExpirationBadges = true;
|
|
|
|
@enumerated
|
|
ThemeMode themeMode = ThemeMode.system;
|
|
|
|
@enumerated
|
|
SortOrder inventorySortOrder = SortOrder.expirationDate;
|
|
}
|
|
|
|
enum ShoppingFrequency {
|
|
weekly, // Every X day of week
|
|
biweekly, // Every 2 weeks on X day
|
|
monthly, // Every month on X date
|
|
payCycle, // Specific dates (15th, 30th)
|
|
custom // User-selected days
|
|
}
|
|
|
|
enum SyncMode {
|
|
localOnly,
|
|
cloudSync,
|
|
wifiSync,
|
|
selfHosted
|
|
}
|
|
|
|
enum ThemeMode {
|
|
light,
|
|
dark,
|
|
system
|
|
}
|
|
|
|
enum SortOrder {
|
|
name,
|
|
expirationDate,
|
|
purchaseDate,
|
|
location,
|
|
quantity
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🔌 API INTEGRATIONS
|
|
|
|
### 1. Open Food Facts API
|
|
|
|
**Purpose:** Auto-populate product info from barcodes
|
|
|
|
**Base URL:** `https://world.openfoodfacts.org/api/v2`
|
|
|
|
**Endpoint:** `GET /product/{barcode}`
|
|
|
|
**Example Request:**
|
|
```dart
|
|
final response = await http.get(
|
|
Uri.parse('https://world.openfoodfacts.org/api/v2/product/0041220576500')
|
|
);
|
|
|
|
// Returns product data including:
|
|
// - product_name
|
|
// - brands
|
|
// - categories
|
|
// - image_url
|
|
// - ingredients
|
|
// - nutriscore
|
|
```
|
|
|
|
**Example Response:**
|
|
```json
|
|
{
|
|
"code": "0041220576500",
|
|
"product": {
|
|
"product_name": "Hidden Valley Ranch Dressing",
|
|
"brands": "Hidden Valley",
|
|
"categories": "Dressings, Ranch dressing",
|
|
"image_url": "https://images.openfoodfacts.org/...",
|
|
"quantity": "16 fl oz (473 mL)"
|
|
},
|
|
"status": 1,
|
|
"status_verbose": "product found"
|
|
}
|
|
```
|
|
|
|
**Service Implementation:**
|
|
```dart
|
|
class OpenFoodFactsService {
|
|
static const baseUrl = 'https://world.openfoodfacts.org/api/v2';
|
|
|
|
Future<ProductInfo?> getProductByBarcode(String barcode) async {
|
|
try {
|
|
final response = await http.get(
|
|
Uri.parse('$baseUrl/product/$barcode')
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
final data = json.decode(response.body);
|
|
if (data['status'] == 1) {
|
|
return ProductInfo.fromJson(data['product']);
|
|
}
|
|
}
|
|
return null; // Product not found
|
|
} catch (e) {
|
|
print('Error fetching product: $e');
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Fallback Strategy:**
|
|
1. Try Open Food Facts
|
|
2. If not found, try UPC Database API (backup)
|
|
3. If still not found, manual entry
|
|
|
|
---
|
|
|
|
### 2. Discord Webhooks
|
|
|
|
**Purpose:** Send expiration alerts to Discord channel
|
|
|
|
**Setup:**
|
|
1. User creates webhook in Discord server settings
|
|
2. Copies webhook URL
|
|
3. Pastes in Sage settings
|
|
4. App sends JSON POST requests to webhook
|
|
|
|
**Example Webhook URL:**
|
|
```
|
|
https://discord.com/api/webhooks/123456789/abcdef...
|
|
```
|
|
|
|
**Sending a Message:**
|
|
```dart
|
|
class DiscordService {
|
|
Future<void> sendAlert(String webhookUrl, List<FoodItem> expiringItems) async {
|
|
final message = _buildAlertMessage(expiringItems);
|
|
|
|
final response = await http.post(
|
|
Uri.parse(webhookUrl),
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: json.encode({
|
|
'username': 'Sage Kitchen Alert',
|
|
'avatar_url': 'https://sage-app.com/icon.png',
|
|
'embeds': [
|
|
{
|
|
'title': '🌿 Sage Alert 🌿',
|
|
'description': message,
|
|
'color': 0x4CAF50, // Green
|
|
'footer': {
|
|
'text': 'Sage Kitchen Management'
|
|
},
|
|
'timestamp': DateTime.now().toIso8601String()
|
|
}
|
|
]
|
|
})
|
|
);
|
|
|
|
if (response.statusCode != 204) {
|
|
throw Exception('Failed to send Discord alert');
|
|
}
|
|
}
|
|
|
|
String _buildAlertMessage(List<FoodItem> items) {
|
|
final buffer = StringBuffer();
|
|
buffer.writeln('@everyone Shopping day is tomorrow!\n');
|
|
buffer.writeln('**Expiring before next week:**');
|
|
|
|
for (final item in items) {
|
|
final emoji = _getEmoji(item.daysUntilExpiration);
|
|
buffer.writeln('$emoji ${item.name} (expires in ${item.daysUntilExpiration} days)');
|
|
}
|
|
|
|
buffer.writeln('\nDon\'t forget to restock! 🛒');
|
|
return buffer.toString();
|
|
}
|
|
|
|
String _getEmoji(int days) {
|
|
if (days <= 3) return '🔴';
|
|
if (days <= 7) return '🟡';
|
|
return '🟢';
|
|
}
|
|
}
|
|
```
|
|
|
|
**Rate Limiting:**
|
|
- Discord allows ~5 requests per second per webhook
|
|
- Batch alerts together
|
|
- Use exponential backoff if rate limited
|
|
|
|
---
|
|
|
|
## 📱 USER FLOWS
|
|
|
|
### Flow 1: First Time Setup
|
|
|
|
```
|
|
User downloads Sage
|
|
↓
|
|
Welcome screen
|
|
↓
|
|
Choose sync mode:
|
|
- Local only (skip to next)
|
|
- Cloud sync (sign up/login)
|
|
- Self-hosted (enter URL)
|
|
↓
|
|
Set shopping frequency:
|
|
"How often do you shop?"
|
|
- Weekly → Pick day
|
|
- Bi-weekly → Pick day
|
|
- Monthly → Pick date
|
|
- Pay cycle → Enter dates
|
|
- Custom → Select days
|
|
↓
|
|
Enable notifications:
|
|
"Get alerts for expiring items?"
|
|
- Yes (request permission)
|
|
- No (can enable later)
|
|
↓
|
|
Optional: Discord setup
|
|
"Send alerts to Discord?"
|
|
- Yes → Paste webhook URL → Test
|
|
- Skip
|
|
↓
|
|
Tutorial:
|
|
"Let's add your first item!"
|
|
- Scan barcode demo
|
|
- OR manual entry
|
|
↓
|
|
Ready to use! 🎉
|
|
```
|
|
|
|
---
|
|
|
|
### Flow 2: Adding Item via Barcode
|
|
|
|
```
|
|
User taps "+ Add Item"
|
|
↓
|
|
Choose method:
|
|
[📷 Scan Barcode] or [✏️ Manual Entry]
|
|
↓
|
|
User taps "Scan Barcode"
|
|
↓
|
|
Camera opens with barcode overlay
|
|
↓
|
|
User scans barcode
|
|
↓
|
|
Query Open Food Facts API
|
|
↓
|
|
┌─────────────────────┐
|
|
│ Product Found? │
|
|
└────┬────────────┬───┘
|
|
│ │
|
|
YES NO
|
|
│ │
|
|
▼ ▼
|
|
Auto-fill: Manual Entry:
|
|
- Name - Type name
|
|
- Photo - Optional photo
|
|
- Category - Select category
|
|
│ │
|
|
└────┬───────┘
|
|
▼
|
|
User adds:
|
|
- Quantity (1, 2, 3...)
|
|
- Unit (bottles, lbs, etc.)
|
|
- Expiration date (calendar picker)
|
|
- Location (dropdown: Fridge/Freezer/Pantry)
|
|
- Optional notes
|
|
↓
|
|
[Save] button
|
|
↓
|
|
Save to local database
|
|
↓
|
|
If cloud sync enabled:
|
|
Queue for sync
|
|
↓
|
|
Show success message
|
|
Navigate to inventory screen
|
|
```
|
|
|
|
---
|
|
|
|
### Flow 3: Meal Planning with Recipes
|
|
|
|
```
|
|
User opens Recipes tab
|
|
↓
|
|
Browses recipes OR taps "What Can I Make?"
|
|
↓
|
|
IF "What Can I Make?":
|
|
System matches recipes to current inventory
|
|
Shows % of ingredients available
|
|
"Caesar Salad: 75% (missing croutons)"
|
|
↓
|
|
User selects recipe
|
|
↓
|
|
Recipe details screen shows:
|
|
- Photo
|
|
- Ingredients with checkmarks:
|
|
✅ Romaine lettuce (in fridge)
|
|
✅ Ranch dressing (in pantry)
|
|
❌ Croutons (not in inventory)
|
|
- Instructions
|
|
- Prep time
|
|
↓
|
|
User taps "Add Missing Items to Shopping List"
|
|
↓
|
|
Dialog: "Add to which list?"
|
|
- Select existing list
|
|
- OR create new list
|
|
↓
|
|
Croutons added to shopping list
|
|
↓
|
|
User cooks meal!
|
|
↓
|
|
Optional: Mark ingredients as used
|
|
(Reduces quantity in inventory)
|
|
```
|
|
|
|
---
|
|
|
|
### Flow 4: Shopping Trip
|
|
|
|
```
|
|
User opens Shopping tab
|
|
↓
|
|
Selects "Costco" shopping list
|
|
↓
|
|
List shows:
|
|
☐ Milk (2 gallons)
|
|
☐ Chicken (2 lbs)
|
|
☐ Croutons (1 bag)
|
|
☑ Eggs (already checked off yesterday)
|
|
↓
|
|
At store, user checks off items:
|
|
☑ Milk
|
|
☑ Chicken
|
|
☑ Croutons
|
|
↓
|
|
All items checked!
|
|
↓
|
|
Banner appears:
|
|
"Shopping complete! Add items to inventory?"
|
|
[Yes] [Not yet]
|
|
↓
|
|
User taps [Yes]
|
|
↓
|
|
Bulk add screen:
|
|
Pre-filled with checked items
|
|
User adds:
|
|
- Expiration dates (smart defaults shown)
|
|
- Locations (remembered from last time)
|
|
Quick swipe interface
|
|
↓
|
|
[Save All] button
|
|
↓
|
|
All items added to inventory
|
|
Shopping list cleared (or moved to archive)
|
|
↓
|
|
Success! 🎉
|
|
```
|
|
|
|
---
|
|
|
|
### Flow 5: Responding to Expiration Alert
|
|
|
|
```
|
|
Morning notification:
|
|
"🌿 Sage: Use soon!
|
|
Ranch dressing expires in 3 days"
|
|
↓
|
|
User taps notification
|
|
↓
|
|
App opens to "Use It Up" screen
|
|
↓
|
|
Shows recipes featuring ranch:
|
|
- Buffalo Chicken Wrap
|
|
- Veggie Dip Platter
|
|
- Ranch Chicken Tacos
|
|
↓
|
|
User selects "Buffalo Chicken Wrap"
|
|
↓
|
|
Checks ingredients:
|
|
✅ Ranch dressing (expiring!)
|
|
✅ Tortillas
|
|
✅ Lettuce
|
|
❌ Chicken (needs to buy)
|
|
↓
|
|
Adds chicken to shopping list
|
|
↓
|
|
Makes dinner, ranch dressing used!
|
|
↓
|
|
User marks ranch as "used up" in app
|
|
OR reduces quantity to 0
|
|
↓
|
|
Item removed from inventory
|
|
Food waste prevented! 💪
|
|
```
|
|
|
|
---
|
|
|
|
## 🔔 NOTIFICATION SYSTEM (DETAILED)
|
|
|
|
### Background Job Architecture
|
|
|
|
**Strategy:** Scheduled daily check + immediate calculations
|
|
|
|
```dart
|
|
class ExpirationTrackerService {
|
|
final InventoryRepository _inventory;
|
|
final SettingsRepository _settings;
|
|
final NotificationService _notifications;
|
|
|
|
Future<void> runDailyCheck() async {
|
|
// Run every morning at 9 AM
|
|
final items = await _inventory.getAllItems();
|
|
final settings = await _settings.getSettings();
|
|
final nextShoppingDay = settings.nextShoppingDay;
|
|
|
|
for (final item in items) {
|
|
await _checkItemAndNotify(item, nextShoppingDay);
|
|
}
|
|
}
|
|
|
|
Future<void> _checkItemAndNotify(FoodItem item, DateTime? nextShopping) async {
|
|
final daysUntil = item.daysUntilExpiration;
|
|
|
|
// 1 month notification
|
|
if (daysUntil == 30) {
|
|
await _notifications.schedule(
|
|
title: '🌿 Sage: FYI',
|
|
body: '${item.name} expires in 30 days',
|
|
payload: 'item:${item.id}',
|
|
);
|
|
}
|
|
|
|
// 2 weeks notification
|
|
if (daysUntil == 14) {
|
|
await _notifications.schedule(
|
|
title: '🌿 Sage: Heads up!',
|
|
body: '${item.name} expires in 2 weeks',
|
|
payload: 'item:${item.id}',
|
|
);
|
|
}
|
|
|
|
// 1 week notification
|
|
if (daysUntil == 7) {
|
|
await _notifications.schedule(
|
|
title: '🌿 Sage: Use soon!',
|
|
body: '${item.name} expires in 1 week - Tap for recipes',
|
|
payload: 'useItUp:${item.id}',
|
|
);
|
|
}
|
|
|
|
// Shopping day check
|
|
if (nextShopping != null) {
|
|
if (item.expirationDate.isBefore(nextShopping) && daysUntil > 0) {
|
|
await _notifications.schedule(
|
|
title: '🌿 Sage: Add to shopping list!',
|
|
body: '${item.name} expires before your next shopping trip',
|
|
payload: 'addToList:${item.id}',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Shopping Day Calculation
|
|
|
|
```dart
|
|
class ShoppingDayCalculator {
|
|
DateTime? calculateNextShoppingDay(UserSettings settings) {
|
|
final now = DateTime.now();
|
|
|
|
switch (settings.frequency) {
|
|
case ShoppingFrequency.weekly:
|
|
return _nextWeekday(now, settings.dayOfWeek!);
|
|
|
|
case ShoppingFrequency.biweekly:
|
|
return _nextBiweekly(now, settings.dayOfWeek!);
|
|
|
|
case ShoppingFrequency.monthly:
|
|
return _nextMonthlyDate(now, settings.dayOfMonth!);
|
|
|
|
case ShoppingFrequency.payCycle:
|
|
return _nextPayCycleDate(now, settings.payCycleDays!);
|
|
|
|
case ShoppingFrequency.custom:
|
|
return _nextCustomDay(now, settings.customDays!);
|
|
}
|
|
}
|
|
|
|
DateTime _nextWeekday(DateTime from, int targetDay) {
|
|
// targetDay: 1 = Monday, 7 = Sunday
|
|
final currentDay = from.weekday;
|
|
int daysToAdd = (targetDay - currentDay) % 7;
|
|
if (daysToAdd == 0) daysToAdd = 7; // Next week
|
|
return from.add(Duration(days: daysToAdd));
|
|
}
|
|
|
|
DateTime _nextBiweekly(DateTime from, int targetDay) {
|
|
final nextWeek = _nextWeekday(from, targetDay);
|
|
// Check if last shopping was less than a week ago
|
|
// If yes, return 2 weeks from last shopping
|
|
// If no, return next week
|
|
// (Would need to store last shopping date)
|
|
return nextWeek.add(Duration(days: 7)); // Simplified
|
|
}
|
|
|
|
DateTime _nextMonthlyDate(DateTime from, int targetDate) {
|
|
var next = DateTime(from.year, from.month, targetDate);
|
|
if (next.isBefore(from) || next.isAtSameMomentAs(from)) {
|
|
// Next month
|
|
next = DateTime(from.year, from.month + 1, targetDate);
|
|
}
|
|
return next;
|
|
}
|
|
|
|
DateTime _nextPayCycleDate(DateTime from, List<int> dates) {
|
|
// dates like [15, 30]
|
|
final upcoming = dates
|
|
.map((d) => DateTime(from.year, from.month, d))
|
|
.where((date) => date.isAfter(from))
|
|
.toList()
|
|
..sort();
|
|
|
|
if (upcoming.isEmpty) {
|
|
// Next month
|
|
final firstDate = dates.first;
|
|
return DateTime(from.year, from.month + 1, firstDate);
|
|
}
|
|
|
|
return upcoming.first;
|
|
}
|
|
|
|
DateTime _nextCustomDay(DateTime from, List<int> days) {
|
|
// days like [1, 4, 6] = Monday, Thursday, Saturday
|
|
final upcoming = days
|
|
.map((d) => _nextWeekday(from, d))
|
|
.toList()
|
|
..sort();
|
|
|
|
return upcoming.first;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🔐 SECURITY & PRIVACY
|
|
|
|
### Data Privacy Principles
|
|
|
|
1. **Local-First:** All data stored locally first, cloud is optional
|
|
2. **User Control:** Users choose sync mode and what to share
|
|
3. **No Tracking:** Zero analytics, no user tracking
|
|
4. **Open Source:** Code is public, verifiable
|
|
5. **Self-Hostable:** Users can run own backend
|
|
|
|
### Cloud Security (Supabase)
|
|
|
|
**Row-Level Security (RLS) Policies:**
|
|
|
|
```sql
|
|
-- Users can only see their own items
|
|
CREATE POLICY "Users see own items"
|
|
ON food_items
|
|
FOR SELECT
|
|
USING (auth.uid() = user_id);
|
|
|
|
-- Household members see shared items
|
|
CREATE POLICY "Household access"
|
|
ON food_items
|
|
FOR SELECT
|
|
USING (
|
|
household_id IN (
|
|
SELECT household_id
|
|
FROM household_members
|
|
WHERE user_id = auth.uid()
|
|
)
|
|
);
|
|
|
|
-- Users can only modify their own items
|
|
CREATE POLICY "Users modify own items"
|
|
ON food_items
|
|
FOR UPDATE
|
|
USING (auth.uid() = user_id);
|
|
|
|
-- Similar policies for recipes, shopping lists, etc.
|
|
```
|
|
|
|
**Authentication:**
|
|
- Email/password (Supabase Auth)
|
|
- Optional: OAuth (Google, Apple) in future
|
|
- JWT tokens for API requests
|
|
- Refresh tokens for sessions
|
|
|
|
### Local Security
|
|
|
|
**Database Encryption:**
|
|
```dart
|
|
// Isar supports encryption
|
|
final isar = await Isar.open(
|
|
[FoodItemSchema, RecipeSchema, ...],
|
|
directory: dir.path,
|
|
inspector: false,
|
|
encryptionKey: _getOrCreateEncryptionKey(),
|
|
);
|
|
```
|
|
|
|
**Secure Storage:**
|
|
- User settings encrypted
|
|
- Discord webhook URL secured
|
|
- Auth tokens in secure storage (flutter_secure_storage)
|
|
|
|
---
|
|
|
|
## 🧪 TESTING STRATEGY
|
|
|
|
### Unit Tests
|
|
Test individual functions and business logic
|
|
|
|
```dart
|
|
// Example: Test expiration calculation
|
|
test('daysUntilExpiration calculates correctly', () {
|
|
final item = FoodItem()
|
|
..name = 'Milk'
|
|
..expirationDate = DateTime.now().add(Duration(days: 5));
|
|
|
|
expect(item.daysUntilExpiration, equals(5));
|
|
});
|
|
|
|
test('expirationStatus returns correct status', () {
|
|
final item = FoodItem()
|
|
..name = 'Milk'
|
|
..expirationDate = DateTime.now().add(Duration(days: 2));
|
|
|
|
expect(item.expirationStatus, equals(ExpirationStatus.critical));
|
|
});
|
|
```
|
|
|
|
### Widget Tests
|
|
Test UI components
|
|
|
|
```dart
|
|
testWidgets('ExpirationBadge shows correct color', (tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: ExpirationBadge(daysUntil: 2),
|
|
),
|
|
);
|
|
|
|
expect(find.text('2 days'), findsOneWidget);
|
|
|
|
final badge = tester.widget<Container>(find.byType(Container));
|
|
expect(badge.color, equals(Colors.red)); // Critical = red
|
|
});
|
|
```
|
|
|
|
### Integration Tests
|
|
Test complete user flows
|
|
|
|
```dart
|
|
testWidgets('Add item flow works end-to-end', (tester) async {
|
|
await tester.pumpWidget(MyApp());
|
|
|
|
// Tap add button
|
|
await tester.tap(find.byIcon(Icons.add));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Choose manual entry
|
|
await tester.tap(find.text('Manual Entry'));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Fill form
|
|
await tester.enterText(find.byKey(Key('name_field')), 'Milk');
|
|
await tester.enterText(find.byKey(Key('quantity_field')), '1');
|
|
|
|
// Select expiration date
|
|
await tester.tap(find.byKey(Key('expiration_picker')));
|
|
// ... date picker interaction
|
|
|
|
// Save
|
|
await tester.tap(find.text('Save'));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Verify item appears in inventory
|
|
expect(find.text('Milk'), findsOneWidget);
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 🚀 DEPLOYMENT
|
|
|
|
### Android Release Process
|
|
|
|
1. **Build Release APK:**
|
|
```bash
|
|
flutter build apk --release
|
|
```
|
|
|
|
2. **Build App Bundle (for Play Store):**
|
|
```bash
|
|
flutter build appbundle --release
|
|
```
|
|
|
|
3. **Sign the Build:**
|
|
- Configure signing in `android/app/build.gradle`
|
|
- Store keystore securely
|
|
- Add to `.gitignore`
|
|
|
|
4. **Upload to Play Store:**
|
|
- Internal testing → Alpha → Beta → Production
|
|
- Gradual rollout recommended
|
|
|
|
### iOS Release Process
|
|
|
|
1. **Prerequisites:**
|
|
- Apple Developer account ($99/year)
|
|
- Xcode installed
|
|
- Provisioning profiles configured
|
|
|
|
2. **Build:**
|
|
```bash
|
|
flutter build ios --release
|
|
```
|
|
|
|
3. **Archive in Xcode:**
|
|
- Open `ios/Runner.xcworkspace`
|
|
- Product → Archive
|
|
- Upload to App Store Connect
|
|
|
|
4. **TestFlight:**
|
|
- Internal testing
|
|
- External beta testing
|
|
- Submit for review
|
|
|
|
### Self-Hosted Backend Deployment
|
|
|
|
**Docker Compose Setup:**
|
|
```yaml
|
|
version: '3.8'
|
|
|
|
services:
|
|
postgres:
|
|
image: postgres:15
|
|
environment:
|
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
|
POSTGRES_DB: sage
|
|
volumes:
|
|
- postgres_data:/var/lib/postgresql/data
|
|
ports:
|
|
- "5432:5432"
|
|
|
|
api:
|
|
image: sage-backend:latest
|
|
depends_on:
|
|
- postgres
|
|
environment:
|
|
DATABASE_URL: postgresql://postgres:${DB_PASSWORD}@postgres:5432/sage
|
|
JWT_SECRET: ${JWT_SECRET}
|
|
ports:
|
|
- "3000:3000"
|
|
|
|
volumes:
|
|
postgres_data:
|
|
```
|
|
|
|
**Deploy on Raspberry Pi:**
|
|
```bash
|
|
# On Raspberry Pi
|
|
git clone https://github.com/sage-app/backend
|
|
cd backend
|
|
cp .env.example .env
|
|
# Edit .env with your settings
|
|
docker-compose up -d
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 METRICS & ANALYTICS
|
|
|
|
### Privacy-Respecting Metrics
|
|
|
|
**What we DON'T track:**
|
|
- Personal data
|
|
- Specific food items
|
|
- User behavior
|
|
- Location data
|
|
|
|
**What we CAN track (locally, opt-in):**
|
|
- Items saved (count only, for user's stats)
|
|
- Food waste prevented (estimated)
|
|
- Money saved (estimated from item costs)
|
|
- App usage (locally, for user dashboard)
|
|
|
|
**User Dashboard:**
|
|
```
|
|
Your Stats (This Month):
|
|
- 🛒 24 items tracked
|
|
- 💰 Estimated savings: $47
|
|
- 🌱 Food waste prevented: 3 lbs
|
|
- 📖 Recipes tried: 5
|
|
- ⭐ Most used recipe: Caesar Salad
|
|
```
|
|
|
|
---
|
|
|
|
## 🎨 DESIGN SYSTEM
|
|
|
|
### Color Palette
|
|
|
|
```dart
|
|
class AppColors {
|
|
// Primary - Sage Green theme
|
|
static const primary = Color(0xFF4CAF50);
|
|
static const primaryDark = Color(0xFF388E3C);
|
|
static const primaryLight = Color(0xFF81C784);
|
|
|
|
// Expiration Status Colors
|
|
static const fresh = Color(0xFF4CAF50); // Green
|
|
static const caution = Color(0xFFFFEB3B); // Yellow
|
|
static const warning = Color(0xFFFF9800); // Orange
|
|
static const critical = Color(0xFFF44336); // Red
|
|
static const expired = Color(0xFF9E9E9E); // Gray
|
|
|
|
// UI
|
|
static const background = Color(0xFFFAFAFA);
|
|
static const surface = Color(0xFFFFFFFF);
|
|
static const text = Color(0xFF212121);
|
|
static const textSecondary = Color(0xFF757575);
|
|
}
|
|
```
|
|
|
|
### Typography
|
|
|
|
```dart
|
|
class AppTextStyles {
|
|
static const headline1 = TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.text,
|
|
);
|
|
|
|
static const headline2 = TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.text,
|
|
);
|
|
|
|
static const body = TextStyle(
|
|
fontSize: 16,
|
|
color: AppColors.text,
|
|
);
|
|
|
|
static const caption = TextStyle(
|
|
fontSize: 12,
|
|
color: AppColors.textSecondary,
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🔮 FUTURE FEATURES
|
|
|
|
### Phase 8+ (Post-MVP)
|
|
|
|
1. **Meal Planning Calendar**
|
|
- Plan meals for the week
|
|
- Auto-generate shopping list
|
|
- Track what you ate
|
|
|
|
2. **Nutrition Tracking**
|
|
- From Open Food Facts nutrition data
|
|
- Track calories, macros (optional)
|
|
- Health insights
|
|
|
|
3. **Price Tracking**
|
|
- Log item costs
|
|
- Track spending over time
|
|
- Price comparison between stores
|
|
|
|
4. **Voice Input**
|
|
- "Hey Sage, add milk to my shopping list"
|
|
- Voice-activated barcode scanning
|
|
|
|
5. **AI Recipe Suggestions**
|
|
- "What can I make with chicken and rice?"
|
|
- Generate recipes from ingredients
|
|
- Dietary restrictions
|
|
|
|
6. **Household Gamification**
|
|
- Compete to waste less food
|
|
- Achievements & badges
|
|
- Family leaderboard
|
|
|
|
7. **Integration with Smart Appliances**
|
|
- Samsung Family Hub fridge
|
|
- Smart scales for quantity tracking
|
|
|
|
---
|
|
|
|
**Built with 💚 by enthusiastic developers who hate wasting food!**
|
|
|
|
**Let's make kitchens smarter, one scan at a time! 🌿**
|