diff --git a/app.rb b/app.rb index e51515d..85a5174 100644 --- a/app.rb +++ b/app.rb @@ -6,6 +6,7 @@ require './app/models/user' require './app/models/area' require './app/models/project' require './app/models/task' +require './app/models/tag' require './app/helpers/authentication_helper' @@ -91,5 +92,7 @@ get '/' do end get '/inbox' do + @tasks = current_user.tasks.incomplete.where(project_id: nil).order(:name) + erb :inbox end diff --git a/app/models/project.rb b/app/models/project.rb index e79f5d7..df37298 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2,4 +2,7 @@ class Project < ActiveRecord::Base belongs_to :user belongs_to :area, optional: true has_many :tasks, dependent: :destroy + + scope :with_incomplete_tasks, -> { joins(:tasks).where(tasks: { completed: false }).distinct } + scope :with_complete_tasks, -> { joins(:tasks).where(tasks: { completed: true }).distinct } end diff --git a/app/models/tag.rb b/app/models/tag.rb new file mode 100644 index 0000000..920e262 --- /dev/null +++ b/app/models/tag.rb @@ -0,0 +1,4 @@ +class Tag < ActiveRecord::Base + belongs_to :user + has_and_belongs_to_many :tasks +end diff --git a/app/models/task.rb b/app/models/task.rb index efc9553..47801ee 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -1,6 +1,8 @@ class Task < ActiveRecord::Base belongs_to :user belongs_to :project, optional: true + has_and_belongs_to_many :tags - default_scope { where(completed: false) } + scope :complete, -> { where(completed: true) } + scope :incomplete, -> { where(completed: false) } end diff --git a/app/models/user.rb b/app/models/user.rb index bfb6931..d3b4b09 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -4,6 +4,7 @@ class User < ActiveRecord::Base has_many :areas has_many :projects has_many :tasks + has_many :tags, dependent: :destroy validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }, uniqueness: true end diff --git a/app/routes/projects_routes.rb b/app/routes/projects_routes.rb index 83069f8..e686680 100644 --- a/app/routes/projects_routes.rb +++ b/app/routes/projects_routes.rb @@ -1,6 +1,7 @@ class Sinatra::Application get '/projects' do - @projects_with_tasks = current_user.projects.includes(:tasks).order(:name) + @projects_with_tasks = current_user.projects.with_incomplete_tasks.order(:name) + @projects_with_tasks_complete = current_user.projects.with_complete_tasks.order(:name) erb :'projects/index' end @@ -14,9 +15,10 @@ class Sinatra::Application post '/project/create' do project = current_user.projects.new( - name: params[:name], + name: params[:name], description: params[:description], - area_id: params[:area_id].presence) + area_id: params[:area_id].presence + ) if project.save redirect '/' diff --git a/app/routes/tasks_routes.rb b/app/routes/tasks_routes.rb index b3dd358..16f7ea5 100644 --- a/app/routes/tasks_routes.rb +++ b/app/routes/tasks_routes.rb @@ -1,26 +1,45 @@ class Sinatra::Application + def update_task_tags(task, tags_json) + return if tags_json.blank? + + begin + tag_names = JSON.parse(tags_json).map { |tag| tag['value'] }.uniq + tags = tag_names.map do |name| + current_user.tags.find_or_create_by(name: name) + end + task.tags = tags + rescue JSON::ParserError + puts "Failed to parse JSON for tags: #{tags_json}" + end + end + get '/tasks' do case params[:due_date] when 'today' today = Date.today - @tasks = current_user.tasks.where(due_date: today.beginning_of_day..today.end_of_day, project: nil) - @projects_with_tasks = current_user.projects - .joins(:tasks) - .where(tasks: { due_date: today.beginning_of_day..today.end_of_day }) + @tasks = current_user.tasks.incomplete.where('due_date <= ?', today.end_of_day).where(project: nil) + + @projects_with_tasks = current_user.projects.with_incomplete_tasks + .where('tasks.due_date <= ?', today.end_of_day) .distinct.order('projects.name ASC') when 'upcoming' one_week_from_today = Date.today + 7.days - @tasks = current_user.tasks.where(due_date: Date.today..one_week_from_today, project: nil) - @projects_with_tasks = current_user.projects.includes(:tasks).where(tasks: { due_date: Date.today..one_week_from_today }).order('projects.name ASC') + @tasks = current_user.tasks.incomplete.where(due_date: Date.today..one_week_from_today, project: nil) + @projects_with_tasks = current_user.projects.with_incomplete_tasks.includes(:tasks).where(tasks: { due_date: Date.today..one_week_from_today }).order('projects.name ASC') when 'never' - @tasks = current_user.tasks.where(due_date: nil, project: nil) - @projects_with_tasks = current_user.projects.includes(:tasks).where(tasks: { due_date: nil }).order('projects.name ASC') + @tasks = current_user.tasks.incomplete.where(due_date: nil, project: nil) + @projects_with_tasks = current_user.projects.with_incomplete_tasks.includes(:tasks).where(tasks: { due_date: nil }).order('projects.name ASC') else - @tasks = current_user.tasks.where(project: nil) - @projects_with_tasks = current_user.projects.includes(:tasks).order('projects.name ASC') + if params[:status] == 'completed' + @tasks = current_user.tasks.complete.where(project: nil) + @projects_with_tasks = current_user.projects.with_complete_tasks.includes(:tasks).order('projects.name ASC') + else + @tasks = current_user.tasks.incomplete.where(project: nil) + @projects_with_tasks = current_user.projects.with_incomplete_tasks.includes(:tasks).order('projects.name ASC') + end end @tasks ||= [] @@ -46,6 +65,7 @@ class Sinatra::Application end if task.save + update_task_tags(task, params[:tags]) redirect request.referrer || '/' else halt 400, 'There was a problem creating the task.' @@ -72,6 +92,7 @@ class Sinatra::Application end if task.update(task_attributes) + update_task_tags(task, params[:tags]) redirect request.referrer || '/' else halt 400, 'There was a problem updating the task.' @@ -80,7 +101,7 @@ class Sinatra::Application patch '/task/:id/toggle_completion' do content_type :json - task = current_user.tasks.find(params[:id]) + task = current_user.tasks.find_by(id: params[:id]) if task task.completed = !task.completed if task.save diff --git a/app/views/inbox.erb b/app/views/inbox.erb index 35b7e9e..5db6a4f 100644 --- a/app/views/inbox.erb +++ b/app/views/inbox.erb @@ -7,10 +7,9 @@ <%= partial :'tasks/_form', locals: { task: Task.new } %> -
- <% standalone_tasks = current_user.tasks.where(project_id: nil) %> - <% if standalone_tasks.any? %> - <% standalone_tasks.each do |task| %> +
+ <% if @tasks %> + <% @tasks.each do |task| %>
<%= partial :'tasks/_form', locals: { task: task } %>
diff --git a/app/views/layout.erb b/app/views/layout.erb index 5c28f40..8cffbab 100644 --- a/app/views/layout.erb +++ b/app/views/layout.erb @@ -7,22 +7,24 @@ + - +
<% if current_user %> <%= partial :'sidebar' %> <%= partial :'projects/_new_project_modal' %> -
+
<%= yield %>
<% else %> -
+
<%= yield %>
<% end %>
+ diff --git a/app/views/projects/index.erb b/app/views/projects/index.erb index 55bb168..ed44e98 100644 --- a/app/views/projects/index.erb +++ b/app/views/projects/index.erb @@ -11,7 +11,7 @@ <% if @projects_with_tasks.any? %> <% @projects_with_tasks.each do |project| %> <% if project.tasks.any? %> -
+
<%= project.name.upcase %>
<% project.tasks.each do |task| %> diff --git a/app/views/projects/show.erb b/app/views/projects/show.erb index b95a319..cf62be7 100644 --- a/app/views/projects/show.erb +++ b/app/views/projects/show.erb @@ -24,7 +24,7 @@ <%= @project.description %>
<% end %> - +
+
+ + +
+
+
+ + <% unless task.new_record? %> + + <% end %> +
+ + + <% if !task.new_record? %> +
+ +
<% end %> -
- - - -<% if !task.new_record? %> -
- -
-<% end %> - - + diff --git a/app/views/tasks/_header.erb b/app/views/tasks/_header.erb index d55d214..bc27b77 100644 --- a/app/views/tasks/_header.erb +++ b/app/views/tasks/_header.erb @@ -4,6 +4,6 @@

Upcoming

<% elsif params[:due_date] == 'never' %>

Someday

-<% elsif params[:due_date] == 'anytime' %> -

Anytime

+<% elsif params[:status] == 'completed' %> +

Completed

<% end %> diff --git a/app/views/tasks/_task.erb b/app/views/tasks/_task.erb index eb3ed6c..26328aa 100644 --- a/app/views/tasks/_task.erb +++ b/app/views/tasks/_task.erb @@ -6,6 +6,13 @@
<%= task.name %>
+
+ <% task.tags.each do |tag| %> + + <%= tag.name %> + + <% end %> +
<% if task.due_date %> <% if task.due_date.to_date == Date.today %> diff --git a/app/views/tasks/index.erb b/app/views/tasks/index.erb index 4e5586b..1bd401f 100644 --- a/app/views/tasks/index.erb +++ b/app/views/tasks/index.erb @@ -8,7 +8,7 @@ <%= partial :'tasks/_form', locals: { task: Task.new } %>
-
+
<% if @tasks.any? %> <% @tasks.each do |task| %>
diff --git a/db/migrate/20231114203847_add_tags.rb b/db/migrate/20231114203847_add_tags.rb new file mode 100644 index 0000000..a6401a7 --- /dev/null +++ b/db/migrate/20231114203847_add_tags.rb @@ -0,0 +1,10 @@ +class AddTags < ActiveRecord::Migration[7.1] + def change + create_table :tags do |t| + t.string :name + t.references :user, null: false, foreign_key: { on_delete: :cascade } + + t.timestamps + end + end +end diff --git a/db/migrate/20231114210336_create_tasks_tags.rb b/db/migrate/20231114210336_create_tasks_tags.rb new file mode 100644 index 0000000..12b6d4a --- /dev/null +++ b/db/migrate/20231114210336_create_tasks_tags.rb @@ -0,0 +1,8 @@ +class CreateTasksTags < ActiveRecord::Migration[7.1] + def change + create_table :tasks_tags, id: false do |t| + t.belongs_to :task, null: false, foreign_key: true + t.belongs_to :tag, null: false, foreign_key: true + end + end +end diff --git a/db/migrate/20231115092055_rename_tasks_tags_to_tags_tasks.rb b/db/migrate/20231115092055_rename_tasks_tags_to_tags_tasks.rb new file mode 100644 index 0000000..4749cd2 --- /dev/null +++ b/db/migrate/20231115092055_rename_tasks_tags_to_tags_tasks.rb @@ -0,0 +1,5 @@ +class RenameTasksTagsToTagsTasks < ActiveRecord::Migration[7.1] + def change + rename_table :tasks_tags, :tags_tasks + end +end diff --git a/db/schema.rb b/db/schema.rb index 4917d52..3065cfc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2023_11_10_163101) do +ActiveRecord::Schema[7.1].define(version: 2023_11_15_092055) do create_table "areas", force: :cascade do |t| t.string "name" t.integer "user_id", null: false @@ -30,6 +30,21 @@ ActiveRecord::Schema[7.1].define(version: 2023_11_10_163101) do t.index ["user_id"], name: "index_projects_on_user_id" end + create_table "tags", force: :cascade do |t| + t.string "name" + t.integer "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_tags_on_user_id" + end + + create_table "tags_tasks", id: false, force: :cascade do |t| + t.integer "task_id", null: false + t.integer "tag_id", null: false + t.index ["tag_id"], name: "index_tags_tasks_on_tag_id" + t.index ["task_id"], name: "index_tags_tasks_on_task_id" + end + create_table "tasks", force: :cascade do |t| t.string "name" t.string "priority" @@ -56,6 +71,9 @@ ActiveRecord::Schema[7.1].define(version: 2023_11_10_163101) do add_foreign_key "areas", "users" add_foreign_key "projects", "areas", on_delete: :cascade add_foreign_key "projects", "users" + add_foreign_key "tags", "users", on_delete: :cascade + add_foreign_key "tags_tasks", "tags" + add_foreign_key "tags_tasks", "tasks" add_foreign_key "tasks", "projects", on_delete: :cascade add_foreign_key "tasks", "users" end diff --git a/public/js/app.js b/public/js/app.js index f1eb3fc..6b307c1 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1,5 +1,6 @@ document.addEventListener("DOMContentLoaded", function () { attachEventListeners(); + new Tagify(document.getElementById('task_tags')); }); function attachEventListeners() { @@ -17,10 +18,13 @@ function attachCollapseListeners() { } function toggleFolderIcon(collapseElement, isOpening) { - const closedFolderIcon = collapseElement.previousElementSibling.querySelector('.bi-folder'); - const openFolderIcon = collapseElement.previousElementSibling.querySelector('.bi-folder2-open'); - closedFolderIcon.classList.toggle('d-none', isOpening); - openFolderIcon.classList.toggle('d-none', !isOpening); + const closedFolderIcon = collapseElement.previousElementSibling?.querySelector('.bi-folder'); + const openFolderIcon = collapseElement.previousElementSibling?.querySelector('.bi-folder2-open'); + + if (closedFolderIcon && openFolderIcon) { + closedFolderIcon.classList.toggle('d-none', isOpening); + openFolderIcon.classList.toggle('d-none', !isOpening); + } } function attachTaskClickListeners() { @@ -40,10 +44,15 @@ function openEditTaskModal(taskId) { return; } const formHtml = formContainer.innerHTML; - document.getElementById('editTaskFormContainer').innerHTML = formHtml; + const editTaskFormContainer = document.getElementById('editTaskFormContainer'); + editTaskFormContainer.innerHTML = formHtml; + + new Tagify(editTaskFormContainer.querySelector('#task_tags')); + new bootstrap.Modal(document.getElementById('editTaskModal')).show(); } + function attachProjectModalListeners() { document.querySelectorAll('[data-bs-toggle="modal"][data-project-id]').forEach(button => { button.addEventListener('click', () => openProjectModalForEdit(button.getAttribute('data-project-id'))); @@ -152,14 +161,17 @@ function updateTaskCompletionStatus(taskId, data) { taskIcon.classList.remove('bi-circle', 'text-warning', 'text-danger'); taskIcon.classList.add('bi-check-circle-fill', 'text-success'); taskDiv.classList.add('opacity-50'); + } else { taskIcon.classList.remove('bi-check-circle-fill', 'text-success'); taskIcon.classList.add('bi-circle'); taskDiv.classList.remove('opacity-50'); applyPriorityColor(taskIcon, data.priority); } + setTimeout(() => taskDiv.remove(), 200); } + function applyPriorityColor(taskIcon, priority) { taskIcon.classList.remove('text-warning', 'text-danger', 'text-secondary'); switch (priority) {