From 2398d2278eb138638bc8d98ce163897da8187abf Mon Sep 17 00:00:00 2001 From: Chris Veleris Date: Mon, 27 Nov 2023 15:09:09 +0200 Subject: [PATCH] Add status, note, project panel --- README.md | 4 +- app.rb | 30 +------ app/helpers/task_helper.rb | 74 ++++++++++++++++ app/models/project.rb | 21 ++++- app/models/task.rb | 7 +- app/routes/notes_routes.rb | 11 ++- app/routes/projects_routes.rb | 3 +- app/routes/tasks_routes.rb | 49 +++++++---- app/views/inbox.erb | 2 +- app/views/notes/_note.erb | 11 +-- app/views/notes/index.erb | 30 ++++--- app/views/projects/_panel.erb | 32 +++++++ app/views/projects/index.erb | 84 ++++++++++++++----- app/views/projects/show.erb | 14 ++-- app/views/sidebar.erb | 6 +- app/views/tasks/_form.erb | 50 +++++++---- app/views/tasks/_header.erb | 2 +- app/views/tasks/_task.erb | 34 +++----- app/views/tasks/index.erb | 8 +- ...20231127092131_change_priority_in_tasks.rb | 37 ++++++++ ...1127094906_add_note_and_status_to_tasks.rb | 23 +++++ db/schema.rb | 7 +- db/seeds.rb | 6 +- public/js/app.js | 2 +- 24 files changed, 396 insertions(+), 151 deletions(-) create mode 100644 app/helpers/task_helper.rb create mode 100644 app/views/projects/_panel.erb create mode 100644 db/migrate/20231127092131_change_priority_in_tasks.rb create mode 100644 db/migrate/20231127094906_add_note_and_status_to_tasks.rb diff --git a/README.md b/README.md index bf36f84..4205236 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ puma -C app/config/puma.rb Pull the latest image: ```bash -docker pull chrisvel/tududi:0.13 +docker pull chrisvel/tududi:0.14 ``` In order to start the docker container you need 3 enviromental variables: @@ -117,7 +117,7 @@ TUDUDI_SESSION_SECRET -e TUDUDI_SESSION_SECRET=3337c138d17ac7acefa412e5db0d7ef6540905b198cc28c5bf0d11e48807a71bdfe48d82ed0a0a6eb667c937cbdd1db3e1e6073b3148bff37f73cc6398a39671 \ -v ~/tududi_db:/usr/src/app/tududi_db \ -p 9292:9292 \ - -d chrisvel/tududi:0.13 + -d chrisvel/tududi:0.14 ``` 3. Navigate to https://localhost:9292 and fill in your email and password. diff --git a/app.rb b/app.rb index 730168e..3d39f30 100644 --- a/app.rb +++ b/app.rb @@ -10,6 +10,7 @@ require './app/models/tag' require './app/models/note' require './app/helpers/authentication_helper' +require './app/helpers/task_helper' require './app/routes/authentication_routes' require './app/routes/tasks_routes' @@ -46,6 +47,8 @@ before do require_login end +helpers TaskHelper + helpers do def current_path request.path_info @@ -55,16 +58,6 @@ helpers do erb page, options.merge!(layout: false) end - def priority_class(task) - return 'text-success' if task.completed - - case task.priority - when 'Medium' then 'text-warning' - when 'High' then 'text-danger' - else 'text-secondary' - end - end - def nav_link_active?(path, query_params = {}, project_id = nil) current_uri = request.path_info current_query = request.query_string @@ -92,23 +85,6 @@ helpers do classes end - def order_name(order_by) - return 'Select' unless order_by - - field, direction = order_by.split(':') - name = case field - when 'due_date' then 'Due Date' - when 'name' then 'Name' - when 'priority' then 'Priority' - when 'created_at' then 'Created At' - else 'Select' - end - - direction_icon = direction == 'asc' ? '' : '' - - "#{name} #{direction_icon}" - end - def update_query_params(key, value) uri = URI(request.url) params = Rack::Utils.parse_nested_query(uri.query) diff --git a/app/helpers/task_helper.rb b/app/helpers/task_helper.rb new file mode 100644 index 0000000..7a5c2ca --- /dev/null +++ b/app/helpers/task_helper.rb @@ -0,0 +1,74 @@ +module TaskHelper + def priority_class(task) + return 'text-success' if task.done? + + case task.priority + when 'medium' + 'text-warning' + when 'high' + 'text-danger' + else + 'text-secondary' + end + end + + def due_date_badge_class(due_date) + return 'bg-light text-dark' unless due_date + + case due_date.to_date + when Date.today + 'bg-primary' + when Date.tomorrow + 'bg-info' + when Date.yesterday..Date.today + 'bg-danger' + else + 'bg-light text-dark' + end + end + + def format_due_date(due_date) + return '' unless due_date + + case due_date.to_date + when Date.today + 'TODAY' + when Date.tomorrow + 'TOMORROW' + when Date.yesterday + 'YESTERDAY' + else + due_date.strftime('%Y-%m-%d') + end + end + + def status_badge_class(status) + case status + when 'not_started' + 'bg-warning-subtle text-warning' + when 'in_progress' + 'bg-primary-subtle text-primary' + when 'done' + 'bg-success-subtle text-success' + else + 'bg-secondary-subtle text-secondary' + end + end + + def order_name(order_by) + return 'Select' unless order_by + + field, direction = order_by.split(':') + name = case field + when 'due_date' then 'Due Date' + when 'name' then 'Name' + when 'priority' then 'Priority' + when 'created_at' then 'Created At' + else 'Select' + end + + direction_icon = direction == 'asc' ? '' : '' + + "#{name} #{direction_icon}" + end +end diff --git a/app/models/project.rb b/app/models/project.rb index f928894..63c37ec 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -4,8 +4,25 @@ class Project < ActiveRecord::Base has_many :tasks, dependent: :destroy has_many :notes, dependent: :destroy - scope :with_incomplete_tasks, -> { joins(:tasks).where(tasks: { completed: false }).distinct } - scope :with_complete_tasks, -> { joins(:tasks).where(tasks: { completed: true }).distinct } + scope :with_incomplete_tasks, -> { joins(:tasks).where.not(tasks: { status: Task.statuses[:done] }).distinct } + scope :with_complete_tasks, -> { joins(:tasks).where(tasks: { status: Task.statuses[:done] }).distinct } validates :name, presence: true + + def task_status_counts + { + total: tasks.count, + in_progress: tasks.where(status: Task.statuses[:in_progress]).count, + done: tasks.where(status: Task.statuses[:done]).count, + not_started: tasks.where(status: Task.statuses[:not_started]).count + } + end + + def progress_percentage + counts = task_status_counts + return 0 if counts[:total].zero? + + completed_tasks = counts[:total] - counts[:not_started] + (completed_tasks.to_f / counts[:total] * 100).round + end end diff --git a/app/models/task.rb b/app/models/task.rb index 907bd8d..80fd896 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -3,8 +3,11 @@ class Task < ActiveRecord::Base belongs_to :project, optional: true has_and_belongs_to_many :tags - scope :complete, -> { where(completed: true) } - scope :incomplete, -> { where(completed: false) } + enum priority: { low: 0, medium: 1, high: 2 } + enum status: { not_started: 0, in_progress: 1, done: 2, archived: 3 } + + scope :complete, -> { where(status: statuses[:done]) } + scope :incomplete, -> { where.not(status: statuses[:done]) } validates :name, presence: true end diff --git a/app/routes/notes_routes.rb b/app/routes/notes_routes.rb index ad1c8d0..2811a5d 100644 --- a/app/routes/notes_routes.rb +++ b/app/routes/notes_routes.rb @@ -16,7 +16,16 @@ class Sinatra::Application get '/notes' do order_by = params[:order_by] || 'title:asc' order_column, order_direction = order_by.split(':') - @notes = current_user.notes.includes(:tags).order("#{order_column} #{order_direction}") + + @notes = current_user.notes.includes(:tags) + @notes = @notes.joins(:tags).where(tags: { name: params[:tag] }) if params[:tag] + @notes = @notes.order("notes.#{order_column} #{order_direction}") + + query_params = Rack::Utils.parse_nested_query(request.query_string) + query_params.delete('tag') + @base_query = query_params.to_query + @base_url = '/notes?' + @base_url += "#{@base_query}&" unless @base_query.empty? erb :'notes/index' end diff --git a/app/routes/projects_routes.rb b/app/routes/projects_routes.rb index c705ab4..84244d7 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, :area).order('name ASC') + @projects_with_tasks = current_user.projects.includes(:tasks, :area).order('areas.name ASC, projects.name ASC') + @grouped_projects = @projects_with_tasks.group_by(&:area) erb :'projects/index' end diff --git a/app/routes/tasks_routes.rb b/app/routes/tasks_routes.rb index 04439ce..386993b 100644 --- a/app/routes/tasks_routes.rb +++ b/app/routes/tasks_routes.rb @@ -15,34 +15,36 @@ module Sinatra end get '/tasks' do - @tasks = current_user.tasks.includes(:project, :tags) + # Base query with necessary joins + base_query = current_user.tasks.includes(:project, :tags) - case params[:due_date] - when 'today' - today = Date.today - @tasks = @tasks.incomplete.where('due_date <= ?', today.end_of_day) - when 'upcoming' - one_week_from_today = Date.today + 7.days - @tasks = @tasks.incomplete.where(due_date: Date.today..one_week_from_today) - when 'never' - @tasks = @tasks.incomplete.where(due_date: nil) - else - @tasks = params[:status] == 'completed' ? @tasks.complete : @tasks.incomplete - end + # Apply filters based on due_date and status + @tasks = case params[:due_date] + when 'today' + base_query.incomplete.where('due_date <= ?', Date.today.end_of_day) + when 'upcoming' + base_query.incomplete.where(due_date: Date.today..(Date.today + 7.days)) + when 'never' + base_query.incomplete.where(due_date: nil) + else + params[:status] == 'done' ? base_query.complete : base_query.incomplete + end + # Apply ordering if params[:order_by] order_column, order_direction = params[:order_by].split(':') order_direction ||= 'asc' @tasks = @tasks.order("tasks.#{order_column} #{order_direction}") end - # Filter by tag if tag parameter is present + # Filter by tag if provided if params[:tag] - tag = params[:tag] - @tasks = @tasks.joins(:tags).where(tags: { name: tag }) + tagged_task_ids = Tag.joins(:tasks).where(name: params[:tag], + tasks: { user_id: current_user.id }).select('tasks.id') + @tasks = @tasks.where(id: tagged_task_ids) end - @tasks = @tasks.to_a.uniq + @tasks = @tasks.distinct erb :'tasks/index' end @@ -52,6 +54,8 @@ module Sinatra name: params[:name], priority: params[:priority], due_date: params[:due_date], + status: params[:status] || Task.statuses[:not_started], + note: params[:note], user_id: current_user.id } @@ -79,6 +83,8 @@ module Sinatra task_attributes = { name: params[:name], priority: params[:priority], + status: params[:status] || Task.statuses[:not_started], + note: params[:note], due_date: params[:due_date] } @@ -101,8 +107,15 @@ module Sinatra patch '/task/:id/toggle_completion' do content_type :json task = current_user.tasks.find_by(id: params[:id]) + if task - task.completed = !task.completed + new_status = if task.done? + task.note.present? ? :in_progress : :not_started + else + :done + end + task.status = new_status + if task.save task.to_json else diff --git a/app/views/inbox.erb b/app/views/inbox.erb index 94effff..6cb5b88 100644 --- a/app/views/inbox.erb +++ b/app/views/inbox.erb @@ -1,6 +1,6 @@

Inbox

-<% unless params[:status] == 'completed' %> +<% unless params[:status] == 'done' %> <%= partial :'tasks/_minimal_form', locals: { task: Task.new } %> <% end %> diff --git a/app/views/notes/_note.erb b/app/views/notes/_note.erb index b60bb29..5377929 100644 --- a/app/views/notes/_note.erb +++ b/app/views/notes/_note.erb @@ -3,18 +3,19 @@
- + <%= note.title %> - <% if note.tags %> + <% if note.tags.any? %>
<% note.tags.each do |tag| %> - + <% tag_url = "#{@base_url}tag=#{tag.name}" %> + <%= tag.name %> - + <% end %>
- <% end %> + <% end %>
diff --git a/app/views/notes/index.erb b/app/views/notes/index.erb index 1a8ecf7..2ad3fd8 100644 --- a/app/views/notes/index.erb +++ b/app/views/notes/index.erb @@ -1,15 +1,25 @@

Notes

-

-
+
+
+
+ + +
-
+
+ <% end %> + +
+
+
+
+ +
@@ -37,18 +54,21 @@
- + +
-
- - <% unless task.new_record? %> - - <% end %> + <% unless task.new_record? %> + + <% end %> +
diff --git a/app/views/tasks/_header.erb b/app/views/tasks/_header.erb index 89adf40..26db1a1 100644 --- a/app/views/tasks/_header.erb +++ b/app/views/tasks/_header.erb @@ -4,7 +4,7 @@

Upcoming

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

Someday

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

Completed

<% else %>

All

diff --git a/app/views/tasks/_task.erb b/app/views/tasks/_task.erb index 0ffc831..8a28714 100644 --- a/app/views/tasks/_task.erb +++ b/app/views/tasks/_task.erb @@ -1,14 +1,14 @@ -
+
-
+
- + <%= task.name %> <% if task.tags.any? %> -
+
<% if task.due_date %> - <% if task.due_date.to_date == Date.today %> - - TODAY - - <% elsif task.due_date.to_date < Date.today %> - - <%= task.due_date.strftime("%Y-%m-%d") %> - - <% elsif task.due_date.to_date == Date.today + 1 %> - - TOMORROW - - <% else %> - - <%= task.due_date.strftime("%Y-%m-%d") %> - - <% end %> + + <%= format_due_date(task.due_date) %> + <% end %> + + + <%= task.status.gsub('_', ' ').capitalize %> +
diff --git a/app/views/tasks/index.erb b/app/views/tasks/index.erb index e0cb596..71b4b6f 100644 --- a/app/views/tasks/index.erb +++ b/app/views/tasks/index.erb @@ -5,7 +5,7 @@ <% if params[:tag] %> <%= params[:tag] %> - + @@ -15,10 +15,10 @@ <%= order_name(params[:order_by]) %>
-<% unless params[:status] == 'completed' %> +<% unless params[:status] == 'done' %> <%= partial :'tasks/_minimal_form', locals: { task: Task.new } %> <% end %>
diff --git a/db/migrate/20231127092131_change_priority_in_tasks.rb b/db/migrate/20231127092131_change_priority_in_tasks.rb new file mode 100644 index 0000000..b1ba953 --- /dev/null +++ b/db/migrate/20231127092131_change_priority_in_tasks.rb @@ -0,0 +1,37 @@ +class ChangePriorityInTasks < ActiveRecord::Migration[7.1] + def up + add_column :tasks, :new_priority, :integer + + Task.reset_column_information + Task.find_each do |task| + new_value = case task.priority + when 'Low' then 0 + when 'Medium' then 1 + when 'High' then 2 + else 0 + end + task.update_column(:new_priority, new_value) + end + + remove_column :tasks, :priority + rename_column :tasks, :new_priority, :priority + end + + def down + add_column :tasks, :old_priority, :string + + Task.reset_column_information + Task.find_each do |task| + old_value = case task.priority + when 0 then 'Low' + when 1 then 'Medium' + when 2 then 'High' + else 'Low' + end + task.update_column(:old_priority, old_value) + end + + remove_column :tasks, :priority + rename_column :tasks, :old_priority, :priority + end +end diff --git a/db/migrate/20231127094906_add_note_and_status_to_tasks.rb b/db/migrate/20231127094906_add_note_and_status_to_tasks.rb new file mode 100644 index 0000000..abf98ef --- /dev/null +++ b/db/migrate/20231127094906_add_note_and_status_to_tasks.rb @@ -0,0 +1,23 @@ +class AddNoteAndStatusToTasks < ActiveRecord::Migration[7.1] + def up + add_column :tasks, :note, :text + add_column :tasks, :status, :integer, default: 0 + + Task.reset_column_information + Task.find_each do |task| + task.update_column(:status, task.completed ? 2 : 0) + end + + remove_column :tasks, :completed + end + + def down + add_column :tasks, :completed, :boolean, default: false + + Task.reset_column_information + Task.where(status: 2).update_all(completed: true) + + remove_column :tasks, :status + remove_column :tasks, :note + end +end diff --git a/db/schema.rb b/db/schema.rb index 20e9274..9c70ea0 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_17_174412) do +ActiveRecord::Schema[7.1].define(version: 2023_11_27_094906) do create_table "areas", force: :cascade do |t| t.string "name" t.integer "user_id", null: false @@ -65,7 +65,6 @@ ActiveRecord::Schema[7.1].define(version: 2023_11_17_174412) do create_table "tasks", force: :cascade do |t| t.string "name" - t.string "priority" t.datetime "due_date" t.integer "user_id", null: false t.integer "project_id" @@ -73,7 +72,9 @@ ActiveRecord::Schema[7.1].define(version: 2023_11_17_174412) do t.datetime "updated_at", null: false t.boolean "today", default: false t.text "description" - t.boolean "completed", default: false + t.integer "priority" + t.text "note" + t.integer "status", default: 0 t.index ["project_id"], name: "index_tasks_on_project_id" t.index ["user_id"], name: "index_tasks_on_user_id" end diff --git a/db/seeds.rb b/db/seeds.rb index 9133790..96188ef 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,6 +1,6 @@ require 'faker' -user = User.create(email: "myemail@somewhere.com", password: "awes0meHax0Rp4ssword") +user = User.create(email: 'myemail@somewhere.com', password: 'awes0meHax0Rp4ssword') user_id = user.id 4.times do @@ -28,10 +28,10 @@ projects.each do |project| 8.times do Task.create( name: Faker::Lorem.sentence(word_count: 3), - priority: ['Low', 'Medium', 'High'].sample, + priority: %w[Low Medium High].sample, due_date: [Date.today, Date.today + rand(1..30), nil].sample, description: Faker::Lorem.sentence(word_count: 15), - completed: [true, false].sample, + status: [0, 1, 2].sample, user_id: user_id, project_id: project.id ) diff --git a/public/js/app.js b/public/js/app.js index 76cc8b8..9376d0e 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -219,7 +219,7 @@ function updateTaskCompletionStatus(taskId, data) { const taskIcon = iconSpan.querySelector('.bi'); const taskDiv = iconSpan.closest('.task-item'); - if (data.completed) { + if (data.done) { taskIcon.classList.remove('bi-circle', 'text-warning', 'text-danger'); taskIcon.classList.add('bi-check-circle-fill', 'text-success'); taskDiv.classList.add('opacity-50');