Project initialize
This commit is contained in:
3
app/assets/config/manifest.js
Normal file
3
app/assets/config/manifest.js
Normal file
@@ -0,0 +1,3 @@
|
||||
//= link_tree ../images
|
||||
//= link_directory ../javascripts .js
|
||||
//= link_directory ../stylesheets .css
|
||||
0
app/assets/images/.keep
Normal file
0
app/assets/images/.keep
Normal file
15
app/assets/javascripts/application.js
Normal file
15
app/assets/javascripts/application.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
||||
// listed below.
|
||||
//
|
||||
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
|
||||
// vendor/assets/javascripts directory can be referenced here using a relative path.
|
||||
//
|
||||
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
||||
// compiled file. JavaScript code in this file should be added after the last require_* statement.
|
||||
//
|
||||
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
|
||||
// about supported directives.
|
||||
//
|
||||
//= require rails-ujs
|
||||
//= require turbolinks
|
||||
//= require_tree .
|
||||
13
app/assets/javascripts/cable.js
Normal file
13
app/assets/javascripts/cable.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// Action Cable provides the framework to deal with WebSockets in Rails.
|
||||
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
|
||||
//
|
||||
//= require action_cable
|
||||
//= require_self
|
||||
//= require_tree ./channels
|
||||
|
||||
(function() {
|
||||
this.App || (this.App = {});
|
||||
|
||||
App.cable = ActionCable.createConsumer();
|
||||
|
||||
}).call(this);
|
||||
0
app/assets/javascripts/channels/.keep
Normal file
0
app/assets/javascripts/channels/.keep
Normal file
3
app/assets/javascripts/departments.coffee
Normal file
3
app/assets/javascripts/departments.coffee
Normal file
@@ -0,0 +1,3 @@
|
||||
# Place all the behaviors and hooks related to the matching controller here.
|
||||
# All this logic will automatically be available in application.js.
|
||||
# You can use CoffeeScript in this file: http://coffeescript.org/
|
||||
25
app/assets/javascripts/tasks.coffee
Normal file
25
app/assets/javascripts/tasks.coffee
Normal file
@@ -0,0 +1,25 @@
|
||||
# Place all the behaviors and hooks related to the matching controller here.
|
||||
# All this logic will automatically be available in application.js.
|
||||
# You can use CoffeeScript in this file: http://coffeescript.org/
|
||||
|
||||
# Toggle description field visibility
|
||||
@toggleDescription = ->
|
||||
descriptionField = document.getElementById('description-field')
|
||||
toggleText = document.getElementById('toggle-text')
|
||||
|
||||
if descriptionField.style.display == 'none'
|
||||
descriptionField.style.display = 'block'
|
||||
toggleText.textContent = '- Hide Description'
|
||||
else
|
||||
descriptionField.style.display = 'none'
|
||||
toggleText.textContent = '+ Add Description'
|
||||
|
||||
# Show description field if it has content on edit pages
|
||||
document.addEventListener 'turbolinks:load', ->
|
||||
descriptionField = document.getElementById('description-field')
|
||||
toggleText = document.getElementById('toggle-text')
|
||||
descriptionTextarea = document.querySelector('.description-textarea')
|
||||
|
||||
if descriptionTextarea && descriptionTextarea.value.trim() != ''
|
||||
descriptionField.style.display = 'block'
|
||||
toggleText.textContent = '- Hide Description' if toggleText
|
||||
1101
app/assets/stylesheets/application.css
Normal file
1101
app/assets/stylesheets/application.css
Normal file
File diff suppressed because it is too large
Load Diff
3
app/assets/stylesheets/departments.scss
Normal file
3
app/assets/stylesheets/departments.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
// Place all the styles related to the Departments controller here.
|
||||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
||||
3
app/assets/stylesheets/tasks.scss
Normal file
3
app/assets/stylesheets/tasks.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
// Place all the styles related to the Tasks controller here.
|
||||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
||||
4
app/channels/application_cable/channel.rb
Normal file
4
app/channels/application_cable/channel.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
module ApplicationCable
|
||||
class Channel < ActionCable::Channel::Base
|
||||
end
|
||||
end
|
||||
4
app/channels/application_cable/connection.rb
Normal file
4
app/channels/application_cable/connection.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
module ApplicationCable
|
||||
class Connection < ActionCable::Connection::Base
|
||||
end
|
||||
end
|
||||
19
app/controllers/application_controller.rb
Normal file
19
app/controllers/application_controller.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
protect_from_forgery with: :exception
|
||||
|
||||
include AuthorizationConcern
|
||||
|
||||
before_action :configure_permitted_parameters, if: :devise_controller?
|
||||
before_action :set_current_user_department
|
||||
|
||||
protected
|
||||
|
||||
def configure_permitted_parameters
|
||||
devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :role, :department_id])
|
||||
devise_parameter_sanitizer.permit(:account_update, keys: [:name, :role, :department_id])
|
||||
end
|
||||
|
||||
def set_current_user_department
|
||||
@current_department = current_user&.department
|
||||
end
|
||||
end
|
||||
0
app/controllers/concerns/.keep
Normal file
0
app/controllers/concerns/.keep
Normal file
56
app/controllers/concerns/authorization_concern.rb
Normal file
56
app/controllers/concerns/authorization_concern.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
module AuthorizationConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :authenticate_user!
|
||||
helper_method :accessible_departments, :accessible_tasks, :accessible_users if respond_to?(:helper_method)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def require_admin!
|
||||
redirect_to root_path, alert: 'Access denied. Admin access required.' unless current_user&.admin?
|
||||
end
|
||||
|
||||
def require_manager!
|
||||
redirect_to root_path, alert: 'Access denied. Manager access required.' unless current_user&.manager? || current_user&.admin?
|
||||
end
|
||||
|
||||
def accessible_departments
|
||||
return Department.all if current_user&.admin?
|
||||
return [current_user.department].compact if current_user&.department
|
||||
Department.none
|
||||
end
|
||||
|
||||
def accessible_tasks
|
||||
return Task.all if current_user&.admin?
|
||||
return Task.by_department(current_user.department) if current_user&.manager?
|
||||
return Task.for_user(current_user) if current_user&.employee?
|
||||
Task.none
|
||||
end
|
||||
|
||||
def accessible_users
|
||||
return User.all if current_user&.admin?
|
||||
return current_user.department&.users || User.none if current_user&.manager?
|
||||
return [current_user] if current_user&.employee?
|
||||
User.none
|
||||
end
|
||||
|
||||
def authorize_task!
|
||||
# Uses @task set by set_task before_action
|
||||
return if current_user&.can_view_task?(@task)
|
||||
redirect_to tasks_path, alert: 'Access denied. You cannot view this task.'
|
||||
end
|
||||
|
||||
def authorize_task_update!
|
||||
# Uses @task set by set_task before_action
|
||||
return if @task.updateable_by?(current_user)
|
||||
redirect_to task_path(@task), alert: 'Access denied. You cannot update this task.'
|
||||
end
|
||||
|
||||
def authorize_task_assignment!
|
||||
# Uses @task set by set_task before_action
|
||||
return if @task.assign?(current_user)
|
||||
redirect_to task_path(@task), alert: 'Access denied. You cannot assign this task.'
|
||||
end
|
||||
end
|
||||
61
app/controllers/departments_controller.rb
Normal file
61
app/controllers/departments_controller.rb
Normal file
@@ -0,0 +1,61 @@
|
||||
class DepartmentsController < ApplicationController
|
||||
before_action :require_admin!, except: [:index, :show]
|
||||
before_action :set_department, only: [:show, :edit, :update, :destroy]
|
||||
before_action :authorize_department!, only: [:show, :edit, :update, :destroy]
|
||||
|
||||
def index
|
||||
@departments = accessible_departments.ordered
|
||||
@department = Department.new
|
||||
end
|
||||
|
||||
def show
|
||||
@tasks = @department.tasks.ordered.includes(:assignee, :creator)
|
||||
@users = @department.users.ordered
|
||||
end
|
||||
|
||||
def new
|
||||
@department = Department.new
|
||||
end
|
||||
|
||||
def create
|
||||
@department = Department.new(department_params)
|
||||
if @department.save
|
||||
redirect_to @department, notice: 'Department was successfully created.'
|
||||
else
|
||||
@departments = accessible_departments.ordered
|
||||
render :index
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
if @department.update(department_params)
|
||||
redirect_to @department, notice: 'Department was successfully updated.'
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@department.destroy
|
||||
redirect_to departments_path, notice: 'Department was successfully deleted.'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_department
|
||||
@department = Department.find(params[:id])
|
||||
end
|
||||
|
||||
def authorize_department!
|
||||
return if current_user&.admin?
|
||||
return if current_user&.can_manage_department?(@department)
|
||||
redirect_to departments_path, alert: 'Access denied.'
|
||||
end
|
||||
|
||||
def department_params
|
||||
params.require(:department).permit(:name, :description)
|
||||
end
|
||||
end
|
||||
116
app/controllers/tasks_controller.rb
Normal file
116
app/controllers/tasks_controller.rb
Normal file
@@ -0,0 +1,116 @@
|
||||
class TasksController < ApplicationController
|
||||
before_action :set_task, only: [:show, :edit, :update, :destroy, :assign]
|
||||
before_action :authorize_task!, only: [:show, :edit]
|
||||
before_action :authorize_task_update!, only: [:update]
|
||||
before_action :authorize_task_assignment!, only: [:assign]
|
||||
|
||||
def index
|
||||
@tasks = accessible_tasks.includes(:assignee, :creator, :department)
|
||||
@departments = accessible_departments.ordered
|
||||
@task = Task.new(department: @current_department)
|
||||
|
||||
# Apply filters
|
||||
@tasks = @tasks.by_department(params[:department_id]) if params[:department_id].present?
|
||||
@tasks = @tasks.by_priority(params[:priority]) if params[:priority].present?
|
||||
@tasks = @tasks.by_status(params[:status]) if params[:status].present?
|
||||
end
|
||||
|
||||
def dashboard
|
||||
@user_stats = current_user.role
|
||||
case current_user.role
|
||||
when 'admin'
|
||||
@total_tasks = Task.count
|
||||
@open_tasks = Task.open.count
|
||||
@urgent_tasks = Task.urgent.count
|
||||
@departments = Department.includes(:users, :tasks)
|
||||
when 'manager'
|
||||
@dept_tasks = Task.by_department(current_user.department)
|
||||
@open_tasks = @dept_tasks.open.count
|
||||
@urgent_tasks = @dept_tasks.urgent.count
|
||||
@team_members = current_user.department.users.includes(:assigned_tasks)
|
||||
when 'employee'
|
||||
@my_tasks = Task.for_user(current_user)
|
||||
@open_tasks = @my_tasks.open.count
|
||||
@urgent_tasks = @my_tasks.urgent.count
|
||||
end
|
||||
|
||||
@recent_tasks = accessible_tasks.ordered.limit(10)
|
||||
end
|
||||
|
||||
def my_tasks
|
||||
@tasks = Task.for_user(current_user).ordered.includes(:assignee, :creator, :department)
|
||||
@task = Task.new(department: @current_department)
|
||||
render :index
|
||||
end
|
||||
|
||||
def show
|
||||
@activities = @task.task_activities.order(created_at: :desc).limit(20)
|
||||
end
|
||||
|
||||
def new
|
||||
@task = Task.new(department: @current_department)
|
||||
@departments = accessible_departments.ordered
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize_task_update!(@task)
|
||||
@departments = accessible_departments.ordered
|
||||
end
|
||||
|
||||
def create
|
||||
@task = Task.new(task_params)
|
||||
@task.creator = current_user
|
||||
@task.department ||= @current_department
|
||||
|
||||
if @task.save
|
||||
redirect_to @task, notice: 'Task was successfully created.'
|
||||
else
|
||||
@tasks = accessible_tasks.ordered
|
||||
@departments = accessible_departments.ordered
|
||||
render :index
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @task.update(task_params)
|
||||
respond_to do |format|
|
||||
format.html { redirect_to @task, notice: 'Task was successfully updated.' }
|
||||
format.js
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.html {
|
||||
@departments = accessible_departments.ordered
|
||||
render :edit
|
||||
}
|
||||
format.js
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def assign
|
||||
assignee = User.find(params[:assignee_id])
|
||||
if @task.assign?(current_user)
|
||||
@task.update(assignee: assignee, status: 'in_progress')
|
||||
redirect_to @task, notice: "Task assigned to #{assignee.name}."
|
||||
else
|
||||
redirect_to @task, alert: 'Access denied. You cannot assign this task.'
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@task.destroy
|
||||
redirect_to tasks_path, notice: 'Task was successfully deleted.'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_task
|
||||
@task = Task.find(params[:id])
|
||||
end
|
||||
|
||||
def task_params
|
||||
permitted = [:title, :description, :priority, :status, :department_id, :assignee_id]
|
||||
params.require(:task).permit(*permitted)
|
||||
end
|
||||
end
|
||||
2
app/helpers/application_helper.rb
Normal file
2
app/helpers/application_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module ApplicationHelper
|
||||
end
|
||||
2
app/helpers/departments_helper.rb
Normal file
2
app/helpers/departments_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module DepartmentsHelper
|
||||
end
|
||||
2
app/helpers/tasks_helper.rb
Normal file
2
app/helpers/tasks_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module TasksHelper
|
||||
end
|
||||
2
app/jobs/application_job.rb
Normal file
2
app/jobs/application_job.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
class ApplicationJob < ActiveJob::Base
|
||||
end
|
||||
4
app/mailers/application_mailer.rb
Normal file
4
app/mailers/application_mailer.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
class ApplicationMailer < ActionMailer::Base
|
||||
default from: 'from@example.com'
|
||||
layout 'mailer'
|
||||
end
|
||||
3
app/models/application_record.rb
Normal file
3
app/models/application_record.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class ApplicationRecord < ActiveRecord::Base
|
||||
self.abstract_class = true
|
||||
end
|
||||
0
app/models/concerns/.keep
Normal file
0
app/models/concerns/.keep
Normal file
20
app/models/department.rb
Normal file
20
app/models/department.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
class Department < ApplicationRecord
|
||||
has_many :users
|
||||
has_many :tasks
|
||||
|
||||
validates :name, presence: true, uniqueness: true
|
||||
|
||||
scope :ordered, -> { order(:name) }
|
||||
|
||||
def user_count
|
||||
users.count
|
||||
end
|
||||
|
||||
def task_count
|
||||
tasks.count
|
||||
end
|
||||
|
||||
def incomplete_task_count
|
||||
tasks.where.not(status: 'completed').count
|
||||
end
|
||||
end
|
||||
105
app/models/task.rb
Normal file
105
app/models/task.rb
Normal file
@@ -0,0 +1,105 @@
|
||||
class Task < ApplicationRecord
|
||||
belongs_to :department, optional: true
|
||||
belongs_to :assignee, class_name: 'User', optional: true
|
||||
belongs_to :creator, class_name: 'User'
|
||||
|
||||
has_many :task_activities, dependent: :destroy
|
||||
|
||||
enum priority: {
|
||||
planning: 'planning',
|
||||
review: 'review',
|
||||
approval: 'approval',
|
||||
execution: 'execution',
|
||||
urgent: 'urgent'
|
||||
}
|
||||
|
||||
enum status: {
|
||||
open: 'open',
|
||||
in_progress: 'in_progress',
|
||||
completed: 'completed',
|
||||
blocked: 'blocked'
|
||||
}
|
||||
|
||||
validates :title, presence: true
|
||||
validates :priority, presence: true
|
||||
validates :status, presence: true
|
||||
validates :creator, presence: true
|
||||
|
||||
scope :ordered, -> { order(priority: :asc, created_at: :desc) }
|
||||
scope :by_priority, ->(priority) { where(priority: priority) if priority.present? }
|
||||
scope :by_status, ->(status) { where(status: status) if status.present? }
|
||||
scope :by_department, ->(department) { where(department: department) if department.present? }
|
||||
scope :by_assignee, ->(assignee) { where(assignee: assignee) if assignee.present? }
|
||||
scope :incomplete, -> { where.not(status: :completed) }
|
||||
scope :complete, -> { where(status: :completed) }
|
||||
scope :urgent, -> { where(priority: :urgent) }
|
||||
scope :for_user, ->(user) {
|
||||
tasks = where(creator: user)
|
||||
tasks = tasks.or(where(assignee: user))
|
||||
tasks
|
||||
}
|
||||
|
||||
def self.incomplete
|
||||
where.not(status: :completed)
|
||||
end
|
||||
|
||||
def self.complete
|
||||
where(status: :completed)
|
||||
end
|
||||
|
||||
def priority_icon
|
||||
case priority
|
||||
when 'planning'
|
||||
'📋'
|
||||
when 'review'
|
||||
'🔍'
|
||||
when 'approval'
|
||||
'⏳'
|
||||
when 'execution'
|
||||
'🚀'
|
||||
when 'urgent'
|
||||
'🔴'
|
||||
end
|
||||
end
|
||||
|
||||
def priority_color
|
||||
case priority
|
||||
when 'planning'
|
||||
'#007bff'
|
||||
when 'review'
|
||||
'#ffc107'
|
||||
when 'approval'
|
||||
'#fd7e14'
|
||||
when 'execution'
|
||||
'#28a745'
|
||||
when 'urgent'
|
||||
'#dc3545'
|
||||
end
|
||||
end
|
||||
|
||||
def status_badge
|
||||
case status
|
||||
when 'open'
|
||||
{ text: 'Open', color: '#6c757d' }
|
||||
when 'in_progress'
|
||||
{ text: 'In Progress', color: '#007bff' }
|
||||
when 'completed'
|
||||
{ text: 'Completed', color: '#28a745' }
|
||||
when 'blocked'
|
||||
{ text: 'Blocked', color: '#dc3545' }
|
||||
end
|
||||
end
|
||||
|
||||
def assign?(user)
|
||||
return true if user.admin?
|
||||
return false unless user.manager?
|
||||
user.can_manage_department?(department)
|
||||
end
|
||||
|
||||
def updateable_by?(user)
|
||||
return true if user.admin?
|
||||
return true if creator == user
|
||||
return true if assignee == user && status.in?(['open', 'in_progress'])
|
||||
false
|
||||
end
|
||||
end
|
||||
4
app/models/task_activity.rb
Normal file
4
app/models/task_activity.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
class TaskActivity < ApplicationRecord
|
||||
belongs_to :task
|
||||
belongs_to :user
|
||||
end
|
||||
45
app/models/user.rb
Normal file
45
app/models/user.rb
Normal file
@@ -0,0 +1,45 @@
|
||||
class User < ApplicationRecord
|
||||
# Include default devise modules. Others available are:
|
||||
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
|
||||
devise :database_authenticatable, :registerable,
|
||||
:recoverable, :rememberable, :validatable,
|
||||
:confirmable, :lockable, :trackable
|
||||
belongs_to :department, optional: true
|
||||
has_many :assigned_tasks, class_name: 'Task', foreign_key: 'assignee_id'
|
||||
has_many :created_tasks, class_name: 'Task', foreign_key: 'creator_id'
|
||||
|
||||
enum role: { admin: 'admin', manager: 'manager', employee: 'employee' }
|
||||
|
||||
validates :name, presence: true
|
||||
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
||||
validates :role, presence: true
|
||||
|
||||
scope :by_role, ->(role) { where(role: role) }
|
||||
scope :by_department, ->(department) { where(department: department) }
|
||||
scope :ordered, -> { order(:name) }
|
||||
|
||||
def can_manage_department?(department)
|
||||
return true if admin?
|
||||
return false unless manager?
|
||||
self.department == department
|
||||
end
|
||||
|
||||
def can_view_task?(task)
|
||||
return true if admin?
|
||||
return true if department == task.department
|
||||
assigned_tasks.include?(task) || created_tasks.include?(task)
|
||||
end
|
||||
|
||||
def can_assign_task?(task)
|
||||
return true if admin?
|
||||
return false unless manager?
|
||||
can_manage_department?(task.department)
|
||||
end
|
||||
|
||||
def department_users
|
||||
return User.all if admin?
|
||||
return department.users if manager?
|
||||
return [self] if employee?
|
||||
User.none
|
||||
end
|
||||
end
|
||||
2
app/views/departments/create.html.erb
Normal file
2
app/views/departments/create.html.erb
Normal file
@@ -0,0 +1,2 @@
|
||||
<h1>Departments#create</h1>
|
||||
<p>Find me in app/views/departments/create.html.erb</p>
|
||||
2
app/views/departments/destroy.html.erb
Normal file
2
app/views/departments/destroy.html.erb
Normal file
@@ -0,0 +1,2 @@
|
||||
<h1>Departments#destroy</h1>
|
||||
<p>Find me in app/views/departments/destroy.html.erb</p>
|
||||
31
app/views/departments/edit.html.erb
Normal file
31
app/views/departments/edit.html.erb
Normal file
@@ -0,0 +1,31 @@
|
||||
<h1>Edit Department</h1>
|
||||
|
||||
<%= form_with(model: @department, local: true) do |form| %>
|
||||
<% if @department.errors.any? %>
|
||||
<div id="error_explanation">
|
||||
<h2><%= pluralize(@department.errors.count, "error") %> prohibited this department from being saved:</h2>
|
||||
<ul>
|
||||
<% @department.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="field">
|
||||
<%= form.label :name %>
|
||||
<%= form.text_field :name, placeholder: "Department name..." %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= form.label :description %>
|
||||
<%= form.text_area :description, placeholder: "Department description...", rows: 4 %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= form.submit "Update Department", class: "submit-btn" %>
|
||||
<%= link_to "Cancel", @department, class: "cancel-btn" %>
|
||||
</div>
|
||||
<% end %>
|
||||
36
app/views/departments/index.html.erb
Normal file
36
app/views/departments/index.html.erb
Normal file
@@ -0,0 +1,36 @@
|
||||
<h1>Departments</h1>
|
||||
|
||||
<% if flash[:notice] %>
|
||||
<div class="notice"><%= flash[:notice] %></div>
|
||||
<% end %>
|
||||
|
||||
<div class="departments-grid">
|
||||
<% if @departments.empty? %>
|
||||
<p class="empty-state">No departments found.</p>
|
||||
<% else %>
|
||||
<% @departments.each do |department| %>
|
||||
<div class="department-card">
|
||||
<h3><%= link_to department.name, department_path(department) %></h3>
|
||||
<p><%= department.description %></p>
|
||||
|
||||
<div class="department-stats">
|
||||
<div class="stat">
|
||||
<strong>Users:</strong> <%= department.user_count %>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<strong>Tasks:</strong> <%= department.task_count %>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<strong>Open:</strong> <%= department.incomplete_task_count %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="actions-section">
|
||||
<% if current_user&.admin? %>
|
||||
<%= link_to 'New Department', new_department_path, class: 'action-btn primary' %>
|
||||
<% end %>
|
||||
</div>
|
||||
31
app/views/departments/new.html.erb
Normal file
31
app/views/departments/new.html.erb
Normal file
@@ -0,0 +1,31 @@
|
||||
<h1>New Department</h1>
|
||||
|
||||
<%= form_with(model: @department, local: true) do |form| %>
|
||||
<% if @department.errors.any? %>
|
||||
<div id="error_explanation">
|
||||
<h2><%= pluralize(@department.errors.count, "error") %> prohibited this department from being saved:</h2>
|
||||
<ul>
|
||||
<% @department.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="field">
|
||||
<%= form.label :name %>
|
||||
<%= form.text_field :name, placeholder: "Department name..." %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= form.label :description %>
|
||||
<%= form.text_area :description, placeholder: "Department description...", rows: 4 %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= form.submit "Create Department", class: "submit-btn" %>
|
||||
<%= link_to "Cancel", departments_path, class: "cancel-btn" %>
|
||||
</div>
|
||||
<% end %>
|
||||
40
app/views/departments/show.html.erb
Normal file
40
app/views/departments/show.html.erb
Normal file
@@ -0,0 +1,40 @@
|
||||
<h1><%= @department.name %></h1>
|
||||
|
||||
<div class="department-info">
|
||||
<p><%= @department.description %></p>
|
||||
|
||||
<div class="department-stats">
|
||||
<h3>Department Statistics</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<strong>Total Users:</strong> <%= @department.user_count %>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<strong>Total Tasks:</strong> <%= @department.task_count %>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<strong>Open Tasks:</strong> <%= @department.incomplete_task_count %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tasks-section">
|
||||
<h2>Department Tasks</h2>
|
||||
<% if @tasks.empty? %>
|
||||
<p class="empty-state">No tasks in this department yet.</p>
|
||||
<% else %>
|
||||
<div class="tasks-list">
|
||||
<% @tasks.each do |task| %>
|
||||
<%= render 'tasks/task', task: task %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="department-actions">
|
||||
<%= link_to 'Back to All Departments', departments_path, class: 'action-btn secondary' %>
|
||||
<% if current_user&.can_manage_department?(@department) %>
|
||||
<%= link_to 'Edit Department', edit_department_path(@department), class: 'action-btn primary' %>
|
||||
<% end %>
|
||||
</div>
|
||||
2
app/views/departments/update.html.erb
Normal file
2
app/views/departments/update.html.erb
Normal file
@@ -0,0 +1,2 @@
|
||||
<h1>Departments#update</h1>
|
||||
<p>Find me in app/views/departments/update.html.erb</p>
|
||||
16
app/views/devise/confirmations/new.html.erb
Normal file
16
app/views/devise/confirmations/new.html.erb
Normal file
@@ -0,0 +1,16 @@
|
||||
<h2>Resend confirmation instructions</h2>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :email %><br />
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit "Resend confirmation instructions" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
@@ -0,0 +1,5 @@
|
||||
<p>Welcome <%= @email %>!</p>
|
||||
|
||||
<p>You can confirm your account email through the link below:</p>
|
||||
|
||||
<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p>
|
||||
7
app/views/devise/mailer/email_changed.html.erb
Normal file
7
app/views/devise/mailer/email_changed.html.erb
Normal file
@@ -0,0 +1,7 @@
|
||||
<p>Hello <%= @email %>!</p>
|
||||
|
||||
<% if @resource.try(:unconfirmed_email?) %>
|
||||
<p>We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.</p>
|
||||
<% else %>
|
||||
<p>We're contacting you to notify you that your email has been changed to <%= @resource.email %>.</p>
|
||||
<% end %>
|
||||
3
app/views/devise/mailer/password_change.html.erb
Normal file
3
app/views/devise/mailer/password_change.html.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<p>Hello <%= @resource.email %>!</p>
|
||||
|
||||
<p>We're contacting you to notify you that your password has been changed.</p>
|
||||
@@ -0,0 +1,8 @@
|
||||
<p>Hello <%= @resource.email %>!</p>
|
||||
|
||||
<p>Someone has requested a link to change your password. You can do this through the link below.</p>
|
||||
|
||||
<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>
|
||||
|
||||
<p>If you didn't request this, please ignore this email.</p>
|
||||
<p>Your password won't change until you access the link above and create a new one.</p>
|
||||
7
app/views/devise/mailer/unlock_instructions.html.erb
Normal file
7
app/views/devise/mailer/unlock_instructions.html.erb
Normal file
@@ -0,0 +1,7 @@
|
||||
<p>Hello <%= @resource.email %>!</p>
|
||||
|
||||
<p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p>
|
||||
|
||||
<p>Click the link below to unlock your account:</p>
|
||||
|
||||
<p><%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %></p>
|
||||
25
app/views/devise/passwords/edit.html.erb
Normal file
25
app/views/devise/passwords/edit.html.erb
Normal file
@@ -0,0 +1,25 @@
|
||||
<h2>Change your password</h2>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
<%= f.hidden_field :reset_password_token %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :password, "New password" %><br />
|
||||
<% if @minimum_password_length %>
|
||||
<em>(<%= @minimum_password_length %> characters minimum)</em><br />
|
||||
<% end %>
|
||||
<%= f.password_field :password, autofocus: true, autocomplete: "new-password" %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :password_confirmation, "Confirm new password" %><br />
|
||||
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit "Change my password" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
16
app/views/devise/passwords/new.html.erb
Normal file
16
app/views/devise/passwords/new.html.erb
Normal file
@@ -0,0 +1,16 @@
|
||||
<h2>Forgot your password?</h2>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :email %><br />
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit "Send me reset password instructions" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
43
app/views/devise/registrations/edit.html.erb
Normal file
43
app/views/devise/registrations/edit.html.erb
Normal file
@@ -0,0 +1,43 @@
|
||||
<h2>Edit <%= resource_name.to_s.humanize %></h2>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :email %><br />
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
|
||||
</div>
|
||||
|
||||
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
|
||||
<div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
|
||||
<% end %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :password %> <i>(leave blank if you don't want to change it)</i><br />
|
||||
<%= f.password_field :password, autocomplete: "new-password" %>
|
||||
<% if @minimum_password_length %>
|
||||
<br />
|
||||
<em><%= @minimum_password_length %> characters minimum</em>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :password_confirmation %><br />
|
||||
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
|
||||
<%= f.password_field :current_password, autocomplete: "current-password" %>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit "Update" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<h3>Cancel my account</h3>
|
||||
|
||||
<div>Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete %></div>
|
||||
|
||||
<%= link_to "Back", :back %>
|
||||
48
app/views/devise/registrations/new.html.erb
Normal file
48
app/views/devise/registrations/new.html.erb
Normal file
@@ -0,0 +1,48 @@
|
||||
<div class="auth-container">
|
||||
<h2>Register New Account</h2>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: "auth-form" }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :name %>
|
||||
<%= f.text_field :name, autofocus: true, autocomplete: "name" %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :email %>
|
||||
<%= f.email_field :email, autocomplete: "email" %>
|
||||
</div>
|
||||
|
||||
<% if current_user&.admin? || !current_user %>
|
||||
<div class="field">
|
||||
<%= f.label :role %>
|
||||
<%= f.select :role, User.roles.keys, { prompt: "Select Role" } %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :department_id %>
|
||||
<%= f.collection_select :department_id, Department.ordered, :id, :name, { prompt: "Select Department" } %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :password %>
|
||||
<% if @minimum_password_length %>
|
||||
<em>(<%= @minimum_password_length %> characters minimum)</em>
|
||||
<% end %>
|
||||
<%= f.password_field :password, autocomplete: "new-password" %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :password_confirmation %>
|
||||
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit "Sign up", class: "auth-submit-btn" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
</div>
|
||||
30
app/views/devise/sessions/new.html.erb
Normal file
30
app/views/devise/sessions/new.html.erb
Normal file
@@ -0,0 +1,30 @@
|
||||
<div class="auth-container">
|
||||
<h2>Sign In</h2>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: "auth-form" }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :email %>
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :password %>
|
||||
<%= f.password_field :password, autocomplete: "current-password" %>
|
||||
</div>
|
||||
|
||||
<% if devise_mapping.rememberable? %>
|
||||
<div class="field checkbox-field">
|
||||
<%= f.check_box :remember_me %>
|
||||
<%= f.label :remember_me %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit "Log in", class: "auth-submit-btn" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
</div>
|
||||
15
app/views/devise/shared/_error_messages.html.erb
Normal file
15
app/views/devise/shared/_error_messages.html.erb
Normal file
@@ -0,0 +1,15 @@
|
||||
<% if resource.errors.any? %>
|
||||
<div id="error_explanation" data-turbo-cache="false">
|
||||
<h2>
|
||||
<%= I18n.t("errors.messages.not_saved",
|
||||
count: resource.errors.count,
|
||||
resource: resource.class.model_name.human.downcase)
|
||||
%>
|
||||
</h2>
|
||||
<ul>
|
||||
<% resource.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
25
app/views/devise/shared/_links.html.erb
Normal file
25
app/views/devise/shared/_links.html.erb
Normal file
@@ -0,0 +1,25 @@
|
||||
<%- if controller_name != 'sessions' %>
|
||||
<%= link_to "Log in", new_session_path(resource_name) %><br />
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
|
||||
<%= link_to "Sign up", new_registration_path(resource_name) %><br />
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
|
||||
<%= link_to "Forgot your password?", new_password_path(resource_name) %><br />
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
|
||||
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br />
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
|
||||
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br />
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.omniauthable? %>
|
||||
<%- resource_class.omniauth_providers.each do |provider| %>
|
||||
<%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), data: { turbo: false } %><br />
|
||||
<% end %>
|
||||
<% end %>
|
||||
16
app/views/devise/unlocks/new.html.erb
Normal file
16
app/views/devise/unlocks/new.html.erb
Normal file
@@ -0,0 +1,16 @@
|
||||
<h2>Resend unlock instructions</h2>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label :email %><br />
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit "Resend unlock instructions" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
49
app/views/layouts/application.html.erb
Normal file
49
app/views/layouts/application.html.erb
Normal file
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Project Management System</title>
|
||||
<%= csrf_meta_tags %>
|
||||
|
||||
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
|
||||
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<% if user_signed_in? %>
|
||||
<nav class="main-nav">
|
||||
<div class="nav-container">
|
||||
<div class="nav-brand">
|
||||
<%= link_to 'Project Manager', root_path, class: 'brand-link' %>
|
||||
</div>
|
||||
|
||||
<div class="nav-menu">
|
||||
<%= link_to 'Dashboard', dashboard_path, class: 'nav-link' %>
|
||||
<%= link_to 'Tasks', tasks_path, class: 'nav-link' %>
|
||||
<% if current_user.admin? || current_user.manager? %>
|
||||
<%= link_to 'Departments', departments_path, class: 'nav-link' %>
|
||||
<% end %>
|
||||
<% if current_user.admin? %>
|
||||
<%= link_to 'Users', new_user_registration_path, class: 'nav-link' %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="nav-user">
|
||||
<span class="user-info">
|
||||
<%= current_user.name %> (<%= current_user.role.humanize %>)
|
||||
</span>
|
||||
<%= link_to 'Sign Out', destroy_user_session_path, method: :delete, class: 'nav-link sign-out' %>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<% end %>
|
||||
|
||||
<% if flash[:notice] %>
|
||||
<div class="notice"><%= flash[:notice] %></div>
|
||||
<% end %>
|
||||
<% if flash[:alert] %>
|
||||
<div class="alert"><%= flash[:alert] %></div>
|
||||
<% end %>
|
||||
|
||||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
||||
13
app/views/layouts/mailer.html.erb
Normal file
13
app/views/layouts/mailer.html.erb
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<style>
|
||||
/* Email styles need to be inline */
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
||||
1
app/views/layouts/mailer.text.erb
Normal file
1
app/views/layouts/mailer.text.erb
Normal file
@@ -0,0 +1 @@
|
||||
<%= yield %>
|
||||
71
app/views/tasks/_form.html.erb
Normal file
71
app/views/tasks/_form.html.erb
Normal file
@@ -0,0 +1,71 @@
|
||||
<% if task.errors.any? %>
|
||||
<div id="error_explanation">
|
||||
<h2><%= pluralize(task.errors.count, "error") %> prohibited this task from being saved:</h2>
|
||||
<ul>
|
||||
<% task.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="field-group">
|
||||
<div class="field">
|
||||
<%= form.label :title %>
|
||||
<%= form.text_field :title, placeholder: "Task title...", class: "title-field" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="field-group">
|
||||
<div class="field">
|
||||
<%= form.label :priority %>
|
||||
<%= form.select :priority, Task.priorities.keys.map { |p| [p.humanize, p] }, { prompt: "Select Priority" }, class: "priority-select" %>
|
||||
</div>
|
||||
|
||||
<% if accessible_departments.count > 1 || current_user&.admin? %>
|
||||
<div class="field">
|
||||
<%= form.label :department_id %>
|
||||
<%= form.collection_select :department_id, accessible_departments, :id, :name, { prompt: "Select Department" }, class: "department-select" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="field-group">
|
||||
<% if current_user&.admin? || current_user&.manager? %>
|
||||
<div class="field">
|
||||
<%= form.label :assignee_id %>
|
||||
<%= form.collection_select :assignee_id, accessible_users, :id, :name, { prompt: "Select Assignee" }, class: "assignee-select" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="field">
|
||||
<%= form.label :status %>
|
||||
<%= form.select :status, Task.statuses.keys.map { |s| [s.humanize, s] }, { prompt: "Select Status" }, class: "status-select" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="description-section">
|
||||
<div class="description-toggle" onclick="toggleDescription()">
|
||||
<span id="toggle-text">+ Add Description</span>
|
||||
</div>
|
||||
<div id="description-field" class="description-field" style="display: none;">
|
||||
<%= form.text_area :description, placeholder: "Add a detailed description...", rows: 4, class: "description-textarea" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if form.object.new_record? %>
|
||||
<div class="actions">
|
||||
<%= form.submit "Create Task", class: "submit-btn" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="actions">
|
||||
<%= form.submit "Update Task", class: "submit-btn" %>
|
||||
<%= link_to "Cancel", task, class: "cancel-btn" %>
|
||||
</div>
|
||||
<% end %>
|
||||
58
app/views/tasks/_task.html.erb
Normal file
58
app/views/tasks/_task.html.erb
Normal file
@@ -0,0 +1,58 @@
|
||||
<div id="task_<%= task.id %>" class="task <%= 'completed' if task.status == 'completed' %>">
|
||||
<div class="task-header">
|
||||
<div class="task-priority" style="color: <%= task.priority_color %>">
|
||||
<%= task.priority_icon %> <%= task.priority.humanize %>
|
||||
</div>
|
||||
<div class="task-status-badge" style="background-color: <%= task.status_badge[:color] %>">
|
||||
<%= task.status_badge[:text] %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= form_with(model: task, local: false, method: :patch, class: "toggle-form") do |form| %>
|
||||
<div class="task-content">
|
||||
<%= form.check_box :status, { checked: task.status == 'completed', onchange: "this.form.submit();" }, { class: "task-checkbox" } %>
|
||||
|
||||
<div class="task-info">
|
||||
<span class="task-title"
|
||||
title="<%= task.description&.truncate(100) || 'No description' %>"
|
||||
data-description="<%= task.description || '' %>">
|
||||
<%= task.title %>
|
||||
</span>
|
||||
<% if task.description.present? %>
|
||||
<span class="description-indicator">📝</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="task-meta">
|
||||
<% if task.department %>
|
||||
<span class="department-tag">
|
||||
<%= task.department.name %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<% if task.assignee %>
|
||||
<span class="assignee-tag">
|
||||
Assigned to: <%= task.assignee.name %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<span class="created-tag">
|
||||
Created: <%= task.created_at.strftime('%m/%d/%Y') %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="task-actions">
|
||||
<%= link_to 'View', task, class: 'details-btn' %>
|
||||
<% if task.updateable_by?(current_user) %>
|
||||
<%= link_to 'Edit', edit_task_path(task), class: 'edit-btn' %>
|
||||
<% end %>
|
||||
<% if task.assign?(current_user) && !task.assignee %>
|
||||
<%= link_to 'Assign', assign_task_path(task), method: :patch, class: 'assign-btn' %>
|
||||
<% end %>
|
||||
<% if task.updateable_by?(current_user) || current_user.admin? %>
|
||||
<%= link_to 'Delete', task, method: :delete, data: { confirm: 'Are you sure?' }, class: 'delete-btn' %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
2
app/views/tasks/create.html.erb
Normal file
2
app/views/tasks/create.html.erb
Normal file
@@ -0,0 +1,2 @@
|
||||
<h1>Tasks#create</h1>
|
||||
<p>Find me in app/views/tasks/create.html.erb</p>
|
||||
128
app/views/tasks/dashboard.html.erb
Normal file
128
app/views/tasks/dashboard.html.erb
Normal file
@@ -0,0 +1,128 @@
|
||||
<div class="dashboard-container">
|
||||
<div class="dashboard-header">
|
||||
<h1>Company Dashboard</h1>
|
||||
<div class="user-welcome">
|
||||
Welcome back, <%= current_user.name %>!
|
||||
<span class="user-role">(<%= current_user.role.humanize %>)</span>
|
||||
<% if current_user.department %>
|
||||
<span class="user-department">- <%= current_user.department.name %> Department</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-content">
|
||||
<div class="stats-section">
|
||||
<h2>Statistics</h2>
|
||||
|
||||
<% case current_user.role %>
|
||||
<% when 'admin' %>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number"><%= @total_tasks %></div>
|
||||
<div class="stat-label">Total Tasks</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number"><%= @open_tasks %></div>
|
||||
<div class="stat-label">Open Tasks</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number"><%= @urgent_tasks %></div>
|
||||
<div class="stat-label">Urgent Tasks</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number"><%= @departments.count %></div>
|
||||
<div class="stat-label">Departments</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="departments-overview">
|
||||
<h3>Departments Overview</h3>
|
||||
<% @departments.each do |department| %>
|
||||
<div class="dept-card">
|
||||
<h4><%= department.name %></h4>
|
||||
<div class="dept-stats">
|
||||
<span><strong>Users:</strong> <%= department.users.count %></span>
|
||||
<span><strong>Tasks:</strong> <%= department.tasks.count %></span>
|
||||
<span><strong>Open:</strong> <%= department.tasks.open.count %></span>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% when 'manager' %>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number"><%= @dept_tasks.count %></div>
|
||||
<div class="stat-label">Department Tasks</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number"><%= @open_tasks %></div>
|
||||
<div class="stat-label">Open Tasks</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number"><%= @urgent_tasks %></div>
|
||||
<div class="stat-label">Urgent Tasks</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number"><%= @team_members.count %></div>
|
||||
<div class="stat-label">Team Members</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="team-overview">
|
||||
<h3>Team Overview - <%= current_user.department.name %></h3>
|
||||
<% @team_members.each do |member| %>
|
||||
<div class="member-card">
|
||||
<div class="member-name"><%= member.name %></div>
|
||||
<div class="member-stats">
|
||||
<span><strong>Assigned:</strong> <%= member.assigned_tasks.count %></span>
|
||||
<span><strong>Open:</strong> <%= member.assigned_tasks.open.count %></span>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% when 'employee' %>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number"><%= @my_tasks.count %></div>
|
||||
<div class="stat-label">My Tasks</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number"><%= @open_tasks %></div>
|
||||
<div class="stat-label">Open Tasks</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number"><%= @urgent_tasks %></div>
|
||||
<div class="stat-label">Urgent Tasks</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number"><%= @my_tasks.complete.count %></div>
|
||||
<div class="stat-label">Completed</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="recent-tasks-section">
|
||||
<h2>Recent Tasks</h2>
|
||||
<% if @recent_tasks.empty? %>
|
||||
<p class="empty-state">No recent tasks found.</p>
|
||||
<% else %>
|
||||
<div class="tasks-list">
|
||||
<% @recent_tasks.each do |task| %>
|
||||
<%= render 'task', task: task %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-actions">
|
||||
<%= link_to 'View All Tasks', tasks_path, class: 'action-btn primary' %>
|
||||
<%= link_to 'Create New Task', new_task_path, class: 'action-btn success' %>
|
||||
<% if current_user.admin? || current_user.manager? %>
|
||||
<%= link_to 'Manage Departments', departments_path, class: 'action-btn secondary' %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
2
app/views/tasks/destroy.html.erb
Normal file
2
app/views/tasks/destroy.html.erb
Normal file
@@ -0,0 +1,2 @@
|
||||
<h1>Tasks#destroy</h1>
|
||||
<p>Find me in app/views/tasks/destroy.html.erb</p>
|
||||
16
app/views/tasks/edit.html.erb
Normal file
16
app/views/tasks/edit.html.erb
Normal file
@@ -0,0 +1,16 @@
|
||||
<h1>Edit Task</h1>
|
||||
|
||||
<% if flash[:notice] %>
|
||||
<div class="notice"><%= flash[:notice] %></div>
|
||||
<% end %>
|
||||
|
||||
<div class="edit-task">
|
||||
<%= form_with(model: @task, local: true, class: "task-form edit-form") do |form| %>
|
||||
<%= render 'form', task: @task, form: form %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="back-link">
|
||||
<%= link_to '← Back to Task', @task %> |
|
||||
<%= link_to '← Back to List', tasks_path %>
|
||||
</div>
|
||||
94
app/views/tasks/index.html.erb
Normal file
94
app/views/tasks/index.html.erb
Normal file
@@ -0,0 +1,94 @@
|
||||
<div class="dashboard-header">
|
||||
<h1>Task Management</h1>
|
||||
|
||||
<% if current_user %>
|
||||
<div class="user-info">
|
||||
<span class="user-role">
|
||||
<%= current_user.name %> - <%= current_user.role.humanize %>
|
||||
<% if current_user.department %>
|
||||
( <%= current_user.department.name %> )
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if flash[:notice] %>
|
||||
<div class="notice"><%= flash[:notice] %></div>
|
||||
<% end %>
|
||||
|
||||
<div class="content-layout">
|
||||
<div class="sidebar">
|
||||
<div class="filter-section">
|
||||
<h3>Filters</h3>
|
||||
|
||||
<% if accessible_departments.count > 1 %>
|
||||
<div class="filter-group">
|
||||
<label>Department</label>
|
||||
<%= form_with(url: tasks_path, method: :get, local: true, class: "filter-form") do |f| %>
|
||||
<%= f.select :department_id,
|
||||
options_for_select([[ "All Departments", "" ]] + accessible_departments.map { |d| [d.name, d.id] },
|
||||
params[:department_id]),
|
||||
{ onchange: "this.form.submit();" },
|
||||
{ class: "filter-select" } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Priority</label>
|
||||
<%= form_with(url: tasks_path, method: :get, local: true, class: "filter-form") do |f| %>
|
||||
<%= f.select :priority,
|
||||
options_for_select([[ "All Priorities", "" ]] + Task.priorities.keys.map { |p| [p.humanize, p] },
|
||||
params[:priority]),
|
||||
{ onchange: "this.form.submit();" },
|
||||
{ class: "filter-select" } %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Status</label>
|
||||
<%= form_with(url: tasks_path, method: :get, local: true, class: "filter-form") do |f| %>
|
||||
<%= f.select :status,
|
||||
options_for_select([[ "All Statuses", "" ]] + Task.statuses.keys.map { |s| [s.humanize, s] },
|
||||
params[:status]),
|
||||
{ onchange: "this.form.submit();" },
|
||||
{ class: "filter-select" } %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions-section">
|
||||
<h3>Actions</h3>
|
||||
<%= link_to 'Dashboard', dashboard_path, class: 'action-btn' %>
|
||||
<%= link_to 'My Tasks', my_tasks_path, class: 'action-btn' if current_user.employee? %>
|
||||
<%= link_to 'Departments', departments_path, class: 'action-btn' if current_user.admin? || current_user.manager? %>
|
||||
<% if current_user.admin? %>
|
||||
<%= link_to 'New User', new_user_registration_path, class: 'action-btn' %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="new-task-section">
|
||||
<h2>Create New Task</h2>
|
||||
<%= form_with(model: @task, local: true, class: "task-form") do |form| %>
|
||||
<%= render 'form', task: @task, form: form %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="tasks-section">
|
||||
<h2>Tasks (<%= @tasks.count %>)</h2>
|
||||
|
||||
<% if @tasks.empty? %>
|
||||
<p class="empty-state">No tasks found with current filters.</p>
|
||||
<% else %>
|
||||
<div class="tasks-list">
|
||||
<% @tasks.each do |task| %>
|
||||
<%= render 'task', task: task %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
41
app/views/tasks/show.html.erb
Normal file
41
app/views/tasks/show.html.erb
Normal file
@@ -0,0 +1,41 @@
|
||||
<div class="task-detail" id="task_detail_<%= @task.id %>">
|
||||
<div class="task-header">
|
||||
<h1 class="task-title <%= 'completed' if @task.completed %>">
|
||||
<%= @task.title %>
|
||||
</h1>
|
||||
<div class="task-meta">
|
||||
<span class="status-badge <%= @task.completed ? 'completed' : 'pending' %>">
|
||||
<%= @task.completed ? '✓ Completed' : '○ Pending' %>
|
||||
</span>
|
||||
<span class="created-date">
|
||||
Created: <%= @task.created_at.strftime('%B %d, %Y at %I:%M %p') %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-description">
|
||||
<h2>Description</h2>
|
||||
<% if @task.description.present? %>
|
||||
<div class="description-content">
|
||||
<%= simple_format(@task.description) %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="no-description">No description provided.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="task-actions">
|
||||
<%= form_with(model: @task, local: false, method: :patch, class: "toggle-status-form") do |form| %>
|
||||
<%= form.hidden_field :completed, value: !@task.completed %>
|
||||
<%= form.submit @task.completed ? 'Mark as Pending' : 'Mark as Completed',
|
||||
class: "status-toggle-btn #{@task.completed ? 'pending' : 'completed'}" %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to 'Edit Task', edit_task_path(@task), class: 'edit-btn' %>
|
||||
<%= link_to 'Back to List', tasks_path, class: 'back-btn' %>
|
||||
|
||||
<%= link_to 'Delete', @task, method: :delete,
|
||||
data: { confirm: 'Are you sure you want to delete this task?' },
|
||||
class: 'delete-btn' %>
|
||||
</div>
|
||||
</div>
|
||||
2
app/views/tasks/update.html.erb
Normal file
2
app/views/tasks/update.html.erb
Normal file
@@ -0,0 +1,2 @@
|
||||
<h1>Tasks#update</h1>
|
||||
<p>Find me in app/views/tasks/update.html.erb</p>
|
||||
22
app/views/tasks/update.js.erb
Normal file
22
app/views/tasks/update.js.erb
Normal file
@@ -0,0 +1,22 @@
|
||||
// Update task list if on index page to preserve sorting
|
||||
var tasksList = document.querySelector('.tasks-list');
|
||||
if (tasksList) {
|
||||
<% @tasks = Task.ordered %>
|
||||
tasksList.innerHTML = '<%= j render partial: "task", collection: @tasks, as: :task %>';
|
||||
|
||||
// Handle empty state if necessary
|
||||
var emptyState = document.querySelector('.empty-state');
|
||||
if (emptyState) {
|
||||
if (<%= @tasks.any? %>) {
|
||||
emptyState.style.display = 'none';
|
||||
} else {
|
||||
emptyState.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update task detail page if present
|
||||
var taskDetail = document.getElementById('task_detail_<%= @task.id %>');
|
||||
if (taskDetail) {
|
||||
taskDetail.outerHTML = '<%= j render template: "tasks/show", formats: [:html], layout: false %>';
|
||||
}
|
||||
Reference in New Issue
Block a user