diff --git a/Gemfile b/Gemfile index e682c9b..dfa4a60 100644 --- a/Gemfile +++ b/Gemfile @@ -18,6 +18,15 @@ gem "require_all", "~> 3.0" gem "standalone_migrations" +gem "tty-prompt" + +gem "tty-font" + +gem "pastel" + +# Generate mock data for database seeds +gem "faker" + # These gems will only be used when we are running the application locally group :development do gem "pry", "~> 0.14.1" diff --git a/Gemfile.lock b/Gemfile.lock index bbe6a3d..de21d50 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -58,6 +58,8 @@ GEM drb (2.2.3) erb (6.0.6) erubi (1.13.1) + faker (3.8.0) + i18n (>= 1.8.11, < 2) i18n (1.15.2) concurrent-ruby (~> 1.0) io-console (0.8.2) @@ -95,6 +97,8 @@ GEM parser (3.3.11.1) ast (~> 2.4.1) racc + pastel (0.8.0) + tty-color (~> 0.5) pp (0.6.4) prettyprint prettyprint (0.2.0) @@ -187,12 +191,24 @@ GEM thor (1.5.0) timeout (0.6.1) tsort (0.2.0) + tty-color (0.6.0) + tty-cursor (0.7.1) + tty-font (0.5.0) + tty-prompt (0.23.1) + pastel (~> 0.8) + tty-reader (~> 0.8) + tty-reader (0.9.0) + tty-cursor (~> 0.7) + tty-screen (~> 0.8) + wisper (~> 2.0) + tty-screen (0.8.2) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.2.0) useragent (0.16.11) + wisper (2.0.1) zeitwerk (2.8.2) PLATFORMS @@ -206,6 +222,8 @@ PLATFORMS DEPENDENCIES activerecord (~> 7.1) database_cleaner (~> 2.0) + faker + pastel pry (~> 0.14.1) rake (~> 13.0) require_all (~> 3.0) @@ -213,6 +231,8 @@ DEPENDENCIES rubocop sqlite3 (~> 1.7) standalone_migrations + tty-font + tty-prompt BUNDLED WITH 2.7.2 diff --git a/README.md b/README.md index d6d0893..6dca303 100644 --- a/README.md +++ b/README.md @@ -1,165 +1,72 @@ -# Phase 3 Project — Single-Script Active Record CLI +# Meal Prep Tracker -## Learning Goals - -- Build a database-backed Ruby application using Active Record -- Design and interact with data models using object-oriented Ruby -- Implement a multi-class CLI frontend -- Practice working with migrations, associations, and validations - -## Introduction - -Congrats on getting through all the material for Phase 3! You've learned how to work with databases, design models with Active Record, and write object-oriented Ruby. Now it's time to bring those skills together into a full project. - -This project will focus on building a Ruby command-line application that reads from and writes to a local SQLite3 database using Active Record — no web server required. - -By the end of the project, you'll have a functioning CLI that lets users interact with your data by creating, viewing, updating, and deleting records from the terminal. +Meal Prep Tracker is a Ruby CLI application built with ActiveRecord and SQLite3. The app allows users to create and manage meal plans, add meals to meal plans, update meal information, delete records, and view meal plan budget summaries from the terminal. ## Requirements -### Models - -- At least two model classes using `ActiveRecord::Base` -- A one-to-many relationship (`has_many` / `belongs_to`) -- At least one model with validations -- Display associated data where appropriate (e.g. listing a parent record's associated children) - -### CLI - -- At least two Ruby classes (e.g. a `Menu` class and a model-specific helper) -- A loop or menu interface -- Ability to create, view, update, and delete records -- Update prompts should display the current value before asking for a new one - -## Planning - -- Plan out your features -- Develop user stories - - "As [ a user ], I want [ to perform this action ] so that [ I can accomplish this goal ]." - - Features should not need you there to explain them to users - - Create a `user-stories.md` file and add your user stories there - -## Project Pitches - -Before you start working on your project, you'll pitch your project idea to your instructors for approval and feedback. - -For your project pitch, you should include: - -- The basic story of your application -- The core features of your MVP -- The data you plan to persist and how you will structure it -- Challenges you expect to face -- How you are meeting the requirements of the project - -**MVP ASAP** — Focus on getting your minimum viable product working first! - -## Example Project Domains - -You could build a **Book Tracker** app: - -- `Author` has many `Books` -- Users can: - - Create a new book - - List all books - - Update book details - - Delete a book - - View books by a specific author - -Or a **Workout Log**: +Before running the application, make sure you have: -- `WorkoutSession` has many `Exercises` -- Users can: - - Log a new workout - - Add exercises - - Update reps/weights - - View or delete past workouts +- Ruby installed +- Bundler installed +- SQLite3 installed -## Getting Started +## Installation -**Fork and clone** this repository to get started. +Clone the repository from GitHub: -Install dependencies: + git clone -```bash -bundle install -``` +Move into the project directory: -Create and migrate the database: + cd -```bash -bundle exec rake db:create -bundle exec rake db:migrate -``` +Install the project dependencies: -Optionally seed the database with starter data: + bundle install -```bash -bundle exec rake seed -``` +## Database Setup -Run your CLI application: +Create the database: -```bash -ruby cli/main.rb -``` + bundle exec rake db:create -## Other Useful Commands +Run the migrations to create the database tables: -Open a Pry console with your models loaded: + bundle exec rake db:migrate -```bash -bundle exec rake console -``` +Seed the database with sample data: -Generate a new migration: + bundle exec rake seed -```bash -bundle exec rake db:create_migration NAME=create_books -``` +## Running the Application -## Project Structure +Start the CLI from the project root: -``` -├── app/ -│ └── models/ # Your Active Record model classes go here -├── cli/ -│ └── main.rb # Entry point — your CLI menu lives here -├── config/ -│ └── environment.rb # Loads gems, DB connection, and models -├── db/ -│ ├── config.yml # Database connection settings -│ ├── migrate/ # Migration files -│ └── seeds.rb # Seed data -└── spec/ # RSpec tests (optional) -``` + ruby cli/cli.rb -## Project Tips +## Useful Development Commands -- Sketch your domain model first using [dbdiagram.io](https://dbdiagram.io/) -- Use `bundle exec rake console` to test your models before building the CLI -- Use `binding.pry` for debugging -- Use `puts` and `pp` or gems like `tty-table` for formatted CLI output +Open a Pry console with the application environment loaded: -## Sample Project + bundle exec rake console -A complete implementation is available on the `sample-project` branch. It demonstrates: +Create a new migration: -- **Pet Tracker** domain with Owners and Pets -- Full CRUD with Active Record and a clean menu-driven CLI -- Object-oriented design with user-friendly output -- All required features including current value prompts for updates + bundle exec rake db:new_migration name=migration_name -To view the sample: +Example: -```bash -git checkout sample-project -``` + bundle exec rake db:new_migration name=create_users -See `SAMPLE_PROJECT_README.md` for detailed documentation. +## Main Features -## Resources +The CLI supports: -- [dbdiagram.io](https://dbdiagram.io/) -- [Active Record Basics](https://guides.rubyonrails.org/active_record_basics.html) -- [Active Record Associations](https://guides.rubyonrails.org/association_basics.html) -- [Active Record Validations](https://guides.rubyonrails.org/active_record_validations.html) +- Viewing users +- Selecting a user +- Adding and deleting users +- Viewing a user’s meal plans and meals +- Adding, updating, and deleting meal plans +- Adding, updating, and deleting meals +- Viewing meal plan budget summaries +- Preventing meals from exceeding a meal plan’s budget \ No newline at end of file diff --git a/app/models/meal.rb b/app/models/meal.rb new file mode 100644 index 0000000..0e9c586 --- /dev/null +++ b/app/models/meal.rb @@ -0,0 +1,3 @@ +class Meal < ActiveRecord:: Base + belongs_to :meal_plan +end \ No newline at end of file diff --git a/app/models/meal_plan.rb b/app/models/meal_plan.rb new file mode 100644 index 0000000..2a19006 --- /dev/null +++ b/app/models/meal_plan.rb @@ -0,0 +1,21 @@ +class MealPlan < ActiveRecord::Base + belongs_to :user + has_many :meals + + def total_estimated_cost + meals.sum(:estimated_cost).to_f + end + + def remaining_budget + budget.to_f - total_estimated_cost + end + + def within_budget?(estimated_cost) + remaining_budget >= estimated_cost.to_f + end + + def can_update_meal_cost?(meal, new_cost) + adjusted_total = total_estimated_cost - meal.estimated_cost.to_f + new_cost.to_f + adjusted_total <= budget.to_f + end +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..a77e487 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,4 @@ +class User < ActiveRecord::Base + has_many :meal_plans + has_many :meals, through: :meal_plans +end diff --git a/cli/cli.rb b/cli/cli.rb new file mode 100644 index 0000000..32434cb --- /dev/null +++ b/cli/cli.rb @@ -0,0 +1,57 @@ +#!/usr/bin/env ruby +require_relative "../config/environment" +require_relative "input_helpers" +require_relative "user_menu" +require_relative "meal_plan_menu" +require_relative "meal_menu" +require "tty-prompt" +require "tty-font" +require "pastel" + +class CLI + def initialize + @prompt = TTY::Prompt.new + @font = TTY::Font.new(:standard) + @pastel = Pastel.new + end + + def run + + print "\e[H\e[2J" + puts @pastel.green(@font.write("Meal Prep Tracker")) + + main_menu + ensure + ActiveRecord::Base.connection_pool.disconnect! if ActiveRecord::Base.connected? + end + + def main_menu + loop do + puts @pastel.dim("—" * 50) + choice = @prompt.select("Main Menu", [ + { name: "View users", value: :view_users }, + { name: "Select user", value: :select_user }, + { name: "Add user", value: :add_user }, + { name: "Delete user", value: :delete_user }, + { name: "Exit", value: :exit }, + ]) + + case choice + when :view_users + list_users + when :select_user + user = select_user + user_menu(user) if user + when :add_user + add_new_user + when :delete_user + delete_user + when :exit + puts @pastel.blue(@font.write("Thank you!")) + break + end + end + end +end + +CLI.new.run diff --git a/cli/input_helpers.rb b/cli/input_helpers.rb new file mode 100644 index 0000000..ba6200c --- /dev/null +++ b/cli/input_helpers.rb @@ -0,0 +1,97 @@ +class CLI + def prompt_required(prompt_text) + loop do + print "#{prompt_text} (or type 'back'): " + input = gets.chomp.strip + + throw(:back) if input.downcase == "back" + return input unless input.empty? + + puts "Input cannot be blank." + end + end + + def prompt_required_text(prompt_text) + loop do + print "#{prompt_text} (or type 'back'): " + input = gets.chomp.strip + + throw(:back) if input.downcase == "back" + return input if input.match?(/[a-zA-Z]/) + + puts "Input cannot be blank and must include text." + end + end + + def prompt_integer(prompt_text) + loop do + print "#{prompt_text} (or type 'back'): " + input = gets.chomp.strip + + throw(:back) if input.downcase == "back" + return input.to_i if input.match?(/^\d+$/) + + puts "Invalid input. Please enter a whole number." + end + end + + def prompt_float(prompt_text) + loop do + print "#{prompt_text} (or type 'back'): " + input = gets.chomp.strip + + throw(:back) if input.downcase == "back" + return input.to_f if input.match?(/^\d+(\.\d+)?$/) + + puts "Invalid input. Please enter a valid number." + end + end + + def prompt_boolean(prompt_text) + @prompt.yes?(prompt_text) + end + + def prompt_prep_time + hours = prompt_integer("Prep hours") + minutes = prompt_integer("Prep minutes") + + (hours * 60) + minutes + end + + def format_prep_time(total_minutes) + hours = total_minutes / 60 + minutes = total_minutes % 60 + + if hours > 0 + "#{hours} hr #{minutes} min" + else + "#{minutes} min" + end + end + + def prompt_date(prompt_text) + loop do + print "#{prompt_text} (or type 'back') (MM/DD/YYYY): " + input = gets.chomp.strip + + throw(:back) if input.downcase == "back" + return input if input.match?(%r{^\d{2}/\d{2}/\d{4}$}) + + puts "Invalid date format. Please enter date as MM/DD/YYYY" + end + end + + def find_user_by_name(name) + User.includes(meal_plans: :meals) + .where("LOWER(name) = ?", name.downcase) + .first + end + + def display_selected_user(user) + puts "\nSelected user: #{user.name} (ID: #{user.id})" + end + + def confirm_action(message) + @prompt.yes?(message) + end +end diff --git a/cli/main.rb b/cli/main.rb deleted file mode 100755 index 33e117f..0000000 --- a/cli/main.rb +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env ruby - -require_relative "../config/environment" - -# TODO: Build your CLI application here! -# -# Requirements: -# - Be object-oriented (at least two classes) -# - Query and update the database directly via Active Record models -# - Parse and display records in a readable format -# - Accept user input and use it to create, update, and delete records -# - Use a loop or menu interface -# - Include current value prompts for updates (e.g. show the current value -# before asking the user what they want to change it to) - -puts "Welcome to your CLI Application!" -puts - -# TODO: Implement your CLI functionality here -puts "TODO: Build your CLI application" diff --git a/cli/meal_menu.rb b/cli/meal_menu.rb new file mode 100644 index 0000000..40f26f7 --- /dev/null +++ b/cli/meal_menu.rb @@ -0,0 +1,177 @@ +class CLI + def meal_menu(user) + meal_plan = select_meal_plan(user) + return unless meal_plan + + catch(:back) do + loop do + choice = @prompt.select("Update meals for #{meal_plan.name}", [ + { name: "Add meal", value: :add }, + { name: "Edit meal", value: :edit }, + { name: "Delete meal", value: :delete }, + { name: "Back", value: :back }, + ]) + + case choice + when :add + add_new_meal(meal_plan) + when :edit + update_meal(meal_plan) + when :delete + delete_meal(meal_plan) + when :back + throw :back + end + end + end + end + + def display_meals(meal_plan) + if meal_plan.meals.any? + meal_plan.meals.each do |meal| + display_meal_details(meal) + end + else + puts "No meals found for this meal plan." + end + end + + def display_meal_details(meal) + puts " -> Meal: #{meal.name} (ID: #{meal.id})" + puts " (meal_type: #{meal.meal_type})" + puts " (prep_time: #{format_prep_time(meal.prep_time)})" + puts " (estimated_cost: $#{meal.estimated_cost})" + puts " (calories: #{meal.calories})" + puts " (prepared: #{meal.prepared})" + puts " (favorite: #{meal.favorite})" + end + + def display_budget_error(meal_plan, message) + puts message + puts "Remaining budget: $#{meal_plan.remaining_budget.round(2)}" + end + + def add_new_meal(meal_plan) + catch(:back) do + name = prompt_required("Meal name") + meal_type = prompt_required_text("Meal type") + prep_time = prompt_prep_time + estimated_cost = prompt_float("Estimated cost") + + unless meal_plan.within_budget?(estimated_cost) + display_budget_error(meal_plan, "This meal would put the meal plan over budget.") + return + end + + calories = prompt_integer("Calories") + favorite = prompt_boolean("Favorite?") + + confirmed = @prompt.yes?("Are you sure you want to add '#{name}' meal to #{meal_plan.name}?") + + if confirmed + new_meal = meal_plan.meals.create( + name: name, + meal_type: meal_type, + prep_time: prep_time, + estimated_cost: estimated_cost, + calories: calories, + favorite: favorite + ) + + puts "Success! '#{new_meal.name}' has been created." + else + puts "Adding meal canceled." + end + + return + end + + puts "\nReturning to the previous menu..." + end + + def select_meal(meal_plan) + display_meals(meal_plan) + + print "Enter a meal ID: " + meal_id = gets.chomp + + meal = meal_plan.meals.find { |m| m.id == meal_id.to_i } + + if meal + meal + else + puts "Meal not found." + nil + end + end + + def update_meal(meal_plan) + if meal_plan.meals.empty? + puts "No meals found for this meal plan." + throw :back + end + + meal = select_meal(meal_plan) + return unless meal + + catch(:back) do + field = @prompt.select( + "Which field do you want to change?", + %w[name meal_type prep_time estimated_cost calories prepared favorite back] + ) + + throw :back if field == "back" + + new_value = + case field + when "name", "meal_type" + prompt_required_text("Enter a new value for #{field}") + when "prep_time" + prompt_prep_time + when "calories" + prompt_integer("Enter a new value for #{field}") + when "estimated_cost" + new_cost = prompt_float("Enter a new value for #{field}") + + unless meal_plan.can_update_meal_cost?(meal, new_cost) + display_budget_error(meal_plan, "This update would put the meal plan over budget.") + throw :back + end + + new_cost + when "prepared", "favorite" + prompt_boolean("#{field.capitalize}?") + end + + if meal.update(field.to_sym => new_value) + puts "Success! Record updated in the database." + else + puts "Update failed: #{meal.errors.full_messages.join(', ')}" + end + end + + puts "\nReturning to the previous menu..." + end + + def delete_meal(meal_plan) + if meal_plan.meals.empty? + puts "\nYou don't have any meals to delete" + throw :back + end + + choices = meal_plan.meals.map do |meal| + { name: "Meal ##{meal.id}: #{meal.name}", value: meal } + end + + selected_meal = @prompt.select("\nSelect a meal to delete:", choices) + + confirmed = @prompt.yes?("Are you absolutely sure you want to delete '#{selected_meal.name}'?") + + if confirmed + selected_meal.destroy + puts "Success! '#{selected_meal.name}' has been deleted." + else + puts "Deletion canceled." + end + end +end diff --git a/cli/meal_plan_menu.rb b/cli/meal_plan_menu.rb new file mode 100644 index 0000000..0c3be7a --- /dev/null +++ b/cli/meal_plan_menu.rb @@ -0,0 +1,143 @@ +class CLI + def meal_plan_menu(user) + loop do + choice = @prompt.select("Update meal plans for #{user.name}", [ + { name: "Add meal plan", value: :add }, + { name: "Edit meal plan", value: :edit }, + { name: "Delete meal plan", value: :delete }, + { name: "Back", value: :back }, + ]) + + case choice + when :add + add_new_meal_plan(user) + when :edit + meal_plan = select_meal_plan(user) + update_meal_plan(meal_plan) if meal_plan + when :delete + meal_plan = select_meal_plan(user) + delete_meal_plan(meal_plan) if meal_plan + when :back + break + end + end + end + + def display_meal_plan_summary(meal_plan) + puts "\n#{meal_plan.name} Summary" + puts "Budget: $#{meal_plan.budget}" + puts "Estimated Cost: $#{meal_plan.total_estimated_cost}" + puts "Remaining Budget: $#{meal_plan.remaining_budget}" + + favorite_meals = meal_plan.meals.where(favorite: true) + + puts "\nFavorite Meals:" + if favorite_meals.any? + favorite_meals.each do |meal| + puts "- #{meal.name}" + end + else + puts "No favorite meals found." + end + end + + def select_meal_plan(user) + if user.meal_plans.empty? + puts "No meal plans found." + return nil + end + + choices = user.meal_plans.map do |plan| + { name: "#{plan.name} (ID: #{plan.id})", value: plan } + end + + meal_plan = @prompt.select("Select a meal plan:", choices) + + meal_plan + end + + def display_selected_meal_plan(meal_plan) + puts "\nSelected Meal Plan:" + puts " -> Meal Plan: #{meal_plan.name}" + puts " -> ID: #{meal_plan.id}" + puts " -> Week Start: #{meal_plan.week_start}" + puts " -> Goal: #{meal_plan.goal}" + puts " -> Budget: $#{meal_plan.budget}" + end + + def add_new_meal_plan(user) + catch(:back) do + name = prompt_required_text("Meal plan name") + week_start = prompt_date("Week start date (or type back to return to menu)") + goal = prompt_required_text("Goal") + budget = prompt_float("Budget") + + confirmed = @prompt.yes?("Are you sure you want to add '#{name}' meal plan for #{user.name}?") + + if confirmed + new_meal_plan = user.meal_plans.create( + name: name, + week_start: week_start, + goal: goal, + budget: budget + ) + + puts "Success! '#{new_meal_plan.name}' has been created." + else + puts "Adding meal plan canceled." + end + + return + end + + puts "\nReturning to the previous menu..." + end + + def update_meal_plan(meal_plan) + catch(:back) do + if meal_plan.meals.empty? + puts "There are no meals in this meal plan." + throw :back + end + + meal = select_meal(meal_plan) + throw :back unless meal + + field = @prompt.select( + "Which field do you want to change?", + %w[name week_start goal budget back] + ) + + throw :back if field == "back" + + new_value = + case field + when "name", "goal" + prompt_required_text("Enter a new value for #{field}") + when "week_start" + prompt_date("Enter a new week start date") + when "budget" + prompt_float("Enter a new value for #{field}") + end + + if meal_plan.update(field.to_sym => new_value) + puts "Success! Record updated in the database." + else + puts "Update failed: #{meal_plan.errors.full_messages.join(', ')}" + end + end + + puts "\nReturning to the previous menu..." + end + + def delete_meal_plan(meal_plan) + confirmed = @prompt.yes?("Are you absolutely sure you want to delete '#{meal_plan.name}'?") + + if confirmed + meal_plan.destroy + puts "Success! '#{meal_plan.name}' has been deleted." + else + puts "Deletion canceled." + end + end +end diff --git a/cli/user_menu.rb b/cli/user_menu.rb new file mode 100644 index 0000000..ab16cd7 --- /dev/null +++ b/cli/user_menu.rb @@ -0,0 +1,84 @@ +class CLI + def list_users + User.all.each do |user| + puts "#{user.id}. #{user.name}" + end + end + + def user_menu(user) + loop do + choice = @prompt.select("\n#{user.name}'s Menu", [ + { name: "View meal plans and meals", value: :view_meal_plans }, + { name: "View meal plan summary", value: :meal_plan_summary }, + { name: "Update meal plans", value: :update_meal_plans }, + { name: "Update meals", value: :update_meals }, + { name: "Back to main menu", value: :back }, + ]) + + case choice + when :view_meal_plans + meal_plan = select_meal_plan(user) + if meal_plan + display_selected_meal_plan(meal_plan) + display_meals(meal_plan) + end + when :meal_plan_summary + meal_plan = select_meal_plan(user) + display_meal_plan_summary(meal_plan) if meal_plan + when :update_meal_plans + meal_plan_menu(user) + when :update_meals + meal_menu(user) + when :back + break + end + end + end + + def select_user + print "Enter a user's name: " + user_name = gets.chomp.downcase + + user = find_user_by_name(user_name) + + if user + display_selected_user(user) + user + else + puts "User not found." + nil + end + end + + def add_new_user + name = prompt_required_text("Add a new user") + + if confirm_action("Are you sure you want to add '#{name}'?") + user = User.create(name: name) + puts "Success! '#{user.name}' has been created." + else + puts "Adding user canceled." + end + end + + def delete_user + print "Enter user's name to remove: " + user_name = gets.chomp.downcase + + user = find_user_by_name(user_name) + if user + display_selected_user(user) + user + else + puts "User not found." + return + end + + if confirm_action("Are you absolutely sure you want to delete '#{user.name}'?") + user.destroy + puts "Success! '#{user.name}' has been deleted." + else + puts "Deletion canceled." + end + end +end diff --git a/db/migrate/20260727163608_create_users.rb b/db/migrate/20260727163608_create_users.rb new file mode 100644 index 0000000..15032b2 --- /dev/null +++ b/db/migrate/20260727163608_create_users.rb @@ -0,0 +1,9 @@ +class CreateUsers < ActiveRecord::Migration[7.1] + def change + create_table :users do |t| + t.string :name, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20260727163633_create_meal_plans.rb b/db/migrate/20260727163633_create_meal_plans.rb new file mode 100644 index 0000000..f63c6e7 --- /dev/null +++ b/db/migrate/20260727163633_create_meal_plans.rb @@ -0,0 +1,13 @@ +class CreateMealPlans < ActiveRecord::Migration[7.1] + def change + create_table :meal_plans do |t| + t.string :name, null: false + t.date :week_start + t.string :goal + t.float :budget + t.bigint :user_id, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20260727163639_create_meals.rb b/db/migrate/20260727163639_create_meals.rb new file mode 100644 index 0000000..d12a410 --- /dev/null +++ b/db/migrate/20260727163639_create_meals.rb @@ -0,0 +1,16 @@ +class CreateMeals < ActiveRecord::Migration[7.1] + def change + create_table :meals do |t| + t.string :name, null: false + t.string :meal_type + t.integer :prep_time + t.float :estimated_cost + t.integer :calories + t.boolean :prepared, default: false, null: false + t.boolean :favorite, default: false, null: false + t.bigint :meal_plan_id, null: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..6c56e69 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,42 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.2].define(version: 2026_07_27_163639) do + create_table "meal_plans", force: :cascade do |t| + t.string "name", null: false + t.date "week_start" + t.string "goal" + t.float "budget" + t.bigint "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "meals", force: :cascade do |t| + t.string "name", null: false + t.string "meal_type" + t.integer "prep_time" + t.float "estimated_cost" + t.integer "calories" + t.boolean "prepared", default: false, null: false + t.boolean "favorite", default: false, null: false + t.bigint "meal_plan_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "users", force: :cascade do |t| + t.string "name", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end +end diff --git a/db/seeds.rb b/db/seeds.rb index 437ef89..0fdc81b 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,7 +1,56 @@ -# frozen_string_literal: true +require "faker" -puts "🌱 Seeding spices..." +puts "Cleaning up database..." +Meal.destroy_all +MealPlan.destroy_all +User.destroy_all -# Seed your database here +puts "Seeding 5 users..." +users = 5.times.map do + User.create!( + name: Faker::Name.name + ) +end -puts "✅ Done seeding!" +puts "Seeding meal plans..." +meal_plans = users.flat_map do |user| + 2.times.map do + MealPlan.create!( + name: "#{Faker::Food.ethnic_category} Meal Plan", + week_start: Faker::Date.forward(days: 30), + goal: ["Meal Prep", "High Protein", "Budget Friendly", "Quick Meals", "Balanced Eating"].sample, + budget: Faker::Number.between(from: 75, to: 200), + user: user + ) + end +end + +puts "Seeding meals..." +meal_plans.each do |meal_plan| + 5.times do + remaining_budget = meal_plan.remaining_budget + break if remaining_budget <= 0 + + estimated_cost = Faker::Number.between( + from: 5, + to: [remaining_budget, 30].min + ).round(2) + + prep_hours = Faker::Number.between(from: 0, to: 2) + prep_minutes = Faker::Number.between(from: 0, to: 59) + prep_time = (prep_hours * 60) + prep_minutes + + Meal.create!( + name: Faker::Food.dish, + meal_type: ["Breakfast", "Lunch", "Dinner", "Snack"].sample, + prep_time: prep_time, + estimated_cost: estimated_cost, + calories: Faker::Number.between(from: 250, to: 900), + prepared: [true, false].sample, + favorite: [true, false].sample, + meal_plan: meal_plan + ) + end +end + +puts "Database seeded successfully!" \ No newline at end of file diff --git a/user_stories.md b/user_stories.md new file mode 100644 index 0000000..e015275 --- /dev/null +++ b/user_stories.md @@ -0,0 +1,31 @@ +# Meal Prep Tracker + +## Description: + +- Meal Prep Tracker is a Ruby backend application built with ActiveRecord that supports creating, updating, and deleting weekly meal plans, organizing meals within those plans, and managing meal data for individual users. + +## Models: + + - User + - MealPlan + - Meal + +## Associations: + +- A User has many MealPlans. +- A User has many Meals through MealPlans. +- A MealPlan belongs to a User. +- A MealPlan has many Meals. +- A Meal belongs to a MealPlan. + +## User Stories + +- View existing users. +- View a user's meal plans. +- Create a new meal plan for a user. +- Update an existing meal plan. +- Delete a meal plan. +- Add meals to a meal plan. +- Update meal information. +- Remove meals from a meal plan. +- View all meals within a meal plan. \ No newline at end of file