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 @@
<%= project.area.name %>
-