Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions app/controllers/role_assignments_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
class RoleAssignmentsController < ApplicationController
before_action :set_role, only: [ :new, :create ]
before_action :set_assignment, only: [ :destroy ]

def new
@assignment = @role.role_assignments.new
@users = User.all
end

def create
@assignment = @role.role_assignments.new(assignment_params)

if @assignment.save
redirect_to role_url(@role), notice: "#{@assignment.user.name} assigned as #{@assignment.assignment_type.humanize}."
else
@users = User.all
render :new, status: :unprocessable_entity
end
end

def destroy
role = @assignment.role
@assignment.destroy
redirect_to role_url(role), notice: "Assignment removed."
end

private

def set_role
@role = Role.find(params[:role_id])
end

def set_assignment
@assignment = RoleAssignment.find(params[:id])
end

def assignment_params
params.require(:role_assignment).permit(:user_id, :assignment_type, :starts_at, :ends_at)
end
end
54 changes: 54 additions & 0 deletions app/controllers/roles_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
class RolesController < ApplicationController
before_action :set_role, only: [ :show, :edit, :update, :destroy ]

def index
@roles_by_group = Role.includes(:role_assignments, :users).group_by(&:group)
end

def show
@assignments = @role.role_assignments.active_assignments.includes(:user)
@tasks = @role.tasks.limit(20)
@time_entries = @role.time_entries.recent.limit(10)
@templates = @role.recurring_task_templates
end

def new
@role = Role.new
end

def create
@role = Role.new(role_params)

if @role.save
redirect_to role_url(@role), notice: "Role was successfully created."
else
render :new, status: :unprocessable_entity
end
end

def edit
end

def update
if @role.update(role_params)
redirect_to role_url(@role), notice: "Role was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end

def destroy
@role.discard
redirect_to roles_url, notice: "Role was deleted."
end

private

def set_role
@role = Role.find(params[:id])
end

def role_params
params.require(:role).permit(:title, :duties, :description, :group, :role_type, :term_length_months)
end
end
2 changes: 1 addition & 1 deletion app/controllers/tasks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def set_users
end

def task_params
params.require(:task).permit(:title, :description, :status, :assigned_to_user_id, :due_date)
params.require(:task).permit(:title, :description, :status, :assigned_to_user_id, :due_date, :role_id)
end

def reorder_pending_tasks
Expand Down
49 changes: 49 additions & 0 deletions app/controllers/time_entries_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

class TimeEntriesController < ApplicationController
before_action :authenticate_user!
before_action :set_role, only: [ :new, :create ]

def index
@time_entries = TimeEntry.for_user(current_user).recent.limit(50)
@monthly_by_role = TimeEntry.for_user(current_user)
.for_month(Date.current.year, Date.current.month)
.group(:role_id)
.sum(:hours)
@roles = Role.where(id: @monthly_by_role.keys)
end

def new
@time_entry = TimeEntry.new(role: @role, logged_on: Date.current, entry_type: "reconciliation")
@tasks = @role.tasks.where(status: %w[active backlog])
end

def create
@time_entry = TimeEntry.new(time_entry_params)
@time_entry.user = current_user
@time_entry.role = @role

if @time_entry.save
redirect_to role_url(@role), notice: "Time logged: #{@time_entry.hours} hours."
else
@tasks = @role.tasks.where(status: %w[active backlog])
render :new, status: :unprocessable_entity
end
end

def destroy
@time_entry = TimeEntry.find(params[:id])
@time_entry.destroy
redirect_to time_entries_url, notice: "Time entry removed."
end

private

def set_role
@role = Role.find(params[:role_id])
end

def time_entry_params
params.require(:time_entry).permit(:hours, :logged_on, :entry_type, :task_id, :note)
end
end
14 changes: 14 additions & 0 deletions app/jobs/generate_recurring_tasks_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class GenerateRecurringTasksJob < ApplicationJob
queue_as :default

def perform
RecurringTaskTemplate.find_each do |template|
next unless template.due_for_generation?

holder = template.role.current_holders.first
next unless holder

template.generate_task!(holder.user)
end
end
end
38 changes: 38 additions & 0 deletions app/models/recurring_task_template.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
class RecurringTaskTemplate < ApplicationRecord
belongs_to :role

FREQUENCIES = %w[daily weekly biweekly monthly quarterly].freeze

validates :title, presence: true
validates :frequency, presence: true, inclusion: { in: FREQUENCIES }

def generate_task!(assigned_user)
task = Task.create!(
title: title,
description: description,
role: role,
user: assigned_user,
assigned_to_user: auto_assign_to_holder ? assigned_user : nil,
status: auto_assign_to_holder ? "active" : "backlog"
)
update!(last_generated_at: Date.current)
task
end

def due_for_generation?
return true if last_generated_at.nil?

case frequency
when "daily"
last_generated_at < Date.current
when "weekly"
last_generated_at < 1.week.ago.to_date
when "biweekly"
last_generated_at < 2.weeks.ago.to_date
when "monthly"
last_generated_at < 1.month.ago.to_date
when "quarterly"
last_generated_at < 3.months.ago.to_date
end
end
end
40 changes: 40 additions & 0 deletions app/models/role.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
class Role < ApplicationRecord
include Discardable
has_paper_trail

acts_as_tenant :community

GROUPS = %w[hoa_officers garden facilities community].freeze
ROLE_TYPES = %w[role committee].freeze

has_many :role_assignments, dependent: :destroy
has_many :users, through: :role_assignments
has_many :tasks, dependent: :nullify
has_many :time_entries, dependent: :destroy
has_many :recurring_task_templates, dependent: :destroy

validates :title, presence: true, uniqueness: { scope: :community_id }
validates :role_type, presence: true, inclusion: { in: ROLE_TYPES }
validates :group, inclusion: { in: GROUPS }, allow_blank: true

scope :roles, -> { where(role_type: "role") }
scope :committees, -> { where(role_type: "committee") }
scope :in_group, ->(group) { where(group: group) }
scope :vacant_roles, -> { where(vacant: true) }
scope :filled, -> { where(vacant: false) }
scope :ordered, -> { order(:group, :title) }

default_scope { ordered }

def current_holders
role_assignments.active_assignments.holders
end

def current_backup
role_assignments.active_assignments.backups.first
end

def update_vacancy!
update_column(:vacant, role_assignments.active_assignments.holders.none?)
end
end
34 changes: 34 additions & 0 deletions app/models/role_assignment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
class RoleAssignment < ApplicationRecord
has_paper_trail

belongs_to :role
belongs_to :user

ASSIGNMENT_TYPES = %w[holder backup co_holder].freeze

validates :assignment_type, presence: true, inclusion: { in: ASSIGNMENT_TYPES }
validates :starts_at, presence: true

after_save :update_role_vacancy
after_destroy :update_role_vacancy

scope :active_assignments, -> { where(active: true) }
scope :holders, -> { where(assignment_type: "holder") }
scope :backups, -> { where(assignment_type: "backup") }
scope :co_holders, -> { where(assignment_type: "co_holder") }
scope :expiring_soon, -> { where(active: true).where("ends_at <= ?", 30.days.from_now.to_date) }

def expired?
ends_at.present? && ends_at < Date.current
end

def expiring_soon?
ends_at.present? && ends_at <= 30.days.from_now.to_date && ends_at >= Date.current
end

private

def update_role_vacancy
role.update_vacancy!
end
end
1 change: 1 addition & 0 deletions app/models/task.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class Task < ApplicationRecord

belongs_to :user
belongs_to :assigned_to_user, class_name: "User", optional: true
belongs_to :role, optional: true

validates :title, presence: true
validates :status, presence: true, inclusion: { in: %w[backlog active completed] }
Expand Down
22 changes: 22 additions & 0 deletions app/models/time_entry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class TimeEntry < ApplicationRecord
belongs_to :user
belongs_to :task, optional: true
belongs_to :role, optional: true

ENTRY_TYPES = %w[task reconciliation].freeze

validates :hours, presence: true, numericality: { greater_than: 0 }
validates :logged_on, presence: true
validates :entry_type, presence: true, inclusion: { in: ENTRY_TYPES }

scope :task_entries, -> { where(entry_type: "task") }
scope :reconciliation_entries, -> { where(entry_type: "reconciliation") }
scope :for_month, ->(year, month) {
start_date = Date.new(year, month, 1)
end_date = start_date.end_of_month
where(logged_on: start_date..end_date)
}
scope :for_user, ->(user) { where(user: user) }
scope :for_role, ->(role) { where(role: role) }
scope :recent, -> { order(logged_on: :desc) }
end
4 changes: 4 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ class User < ApplicationRecord
has_many :meal_rsvps, dependent: :destroy
has_many :rsvped_meals, through: :meal_rsvps, source: :meal

# Roles
has_many :role_assignments, dependent: :destroy
has_many :roles, through: :role_assignments

# Notifications
has_many :push_subscriptions, dependent: :destroy
has_many :in_app_notifications, dependent: :destroy
Expand Down
13 changes: 13 additions & 0 deletions app/models/workload_sentiment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class WorkloadSentiment < ApplicationRecord
belongs_to :user
belongs_to :role

SENTIMENTS = %w[too_much just_right too_little].freeze

validates :sentiment, presence: true, inclusion: { in: SENTIMENTS }
validates :month, presence: true
validates :user_id, uniqueness: { scope: [ :role_id, :month ] }

scope :for_month, ->(date) { where(month: date.beginning_of_month) }
scope :for_role, ->(role) { where(role: role) }
end
36 changes: 36 additions & 0 deletions app/views/role_assignments/_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<%= form_with(model: [ role, assignment ]) do |form| %>
<% if assignment.errors.any? %>
<div class="alert alert-error mb-4">
<ul>
<% assignment.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>

<div class="form-control w-full mb-4">
<%= form.label :user_id, "Person", class: "label" %>
<%= form.select :user_id, @users.map { |u| [u.name, u.id] }, { include_blank: "Select person" }, class: "select select-bordered w-full" %>
</div>

<div class="form-control w-full mb-4">
<%= form.label :assignment_type, "Role Type", class: "label" %>
<%= form.select :assignment_type, RoleAssignment::ASSIGNMENT_TYPES.map { |t| [t.humanize, t] }, {}, class: "select select-bordered w-full" %>
</div>

<div class="form-control w-full mb-4">
<%= form.label :starts_at, "Start Date", class: "label" %>
<%= form.date_field :starts_at, class: "input input-bordered w-full", value: assignment.starts_at || Date.current %>
</div>

<div class="form-control w-full mb-4">
<%= form.label :ends_at, "End Date (optional)", class: "label" %>
<%= form.date_field :ends_at, class: "input input-bordered w-full", value: assignment.ends_at || (role.term_length_months ? Date.current + role.term_length_months.months : nil) %>
</div>

<div class="flex justify-end gap-2 mt-6">
<%= link_to "Cancel", role_path(role), class: "btn btn-ghost" %>
<%= form.submit "Assign", class: "btn btn-primary" %>
</div>
<% end %>
4 changes: 4 additions & 0 deletions app/views/role_assignments/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div class="<%= hotwire_native_app? ? '' : '-mx-2 sm:mx-0 px-2 sm:px-0' %>">
<h1 class="text-xl font-bold mb-4">Assign: <%= @role.title %></h1>
<%= render "role_assignments/form", assignment: @assignment, role: @role %>
</div>
Loading
Loading