Compare commits

...

5 Commits

Author SHA1 Message Date
Zin Bo Thit
78450f8348 git conflict fix 2026-01-29 12:28:09 +06:30
Zin Bo Thit
87cc77e0fb Move github source to private server 2026-01-29 12:00:13 +06:30
Zin Bo Thit
417600b23e Fix merge conflict in README 2026-01-28 10:56:40 +06:30
ZINBOTHIT
ff99b29c4f Initial commit 2026-01-28 09:55:47 +06:30
Zin Bo Thit
e8380c6e23 Project initialize 2026-01-28 09:53:14 +06:30
151 changed files with 5111 additions and 1 deletions

14
.dockerignore Normal file
View File

@@ -0,0 +1,14 @@
.git
.gitignore
log/*
tmp/*
db/*.sqlite3
db/*.sqlite3-journal
public/assets
node_modules
.bundle
Dockerfile
docker-compose.yml
README.md
test/*
spec/*

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
# Ignore bundler config.
/.bundle
# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3-journal
# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep
/node_modules
/yarn-error.log
.byebug_history

41
Dockerfile Normal file
View File

@@ -0,0 +1,41 @@
# Use Ruby 2.6.5 as the base image
FROM ruby:2.6.5
# Install system dependencies
RUN apt-get update -qq && apt-get install -y \
build-essential \
libpq-dev \
nodejs \
libsqlite3-dev \
yarn
# Set the working directory
WORKDIR /app
# Install bundler
RUN gem install bundler -v 2.1.4
# Copy Gemfile and Gemfile.lock
COPY Gemfile Gemfile.lock ./
# Install dependencies
RUN bundle install
# Copy the rest of the application code
COPY . .
# Set environment variables
ENV RAILS_ENV=production
ENV RAILS_SERVE_STATIC_FILES=true
ENV RAILS_LOG_TO_STDOUT=true
# Precompile assets
# Note: SECRET_KEY_BASE is required for asset precompilation in some Rails versions
# You can provide a dummy value here if it's not strictly checked during precompile
RUN bundle exec rake assets:precompile SECRET_KEY_BASE=dummy_key
# Expose port 8080 (Cloud Run default)
EXPOSE 8080
# Start the application
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

57
Gemfile Normal file
View File

@@ -0,0 +1,57 @@
source 'http://rubygems.org'
git_source(:github) do |repo_name|
repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
"https://github.com/#{repo_name}.git"
end
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.1.0'
# Use PostgreSQL as the database for Active Record
gem 'pg'
# Use Puma as the app server
gem 'puma', '~> 3.7'
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# See https://github.com/rails/execjs#readme for more supported runtimes
# gem 'therubyracer', platforms: :ruby
# Use CoffeeScript for .coffee assets and views
gem 'coffee-rails', '~> 4.2'
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.5'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 3.0'
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'
# Authentication
gem 'devise', '~> 4.7'
# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
# Adds support for Capybara system testing and selenium driver
gem 'capybara', '~> 2.13.0'
gem 'selenium-webdriver'
end
group :development do
# Access an IRB console on exception pages or by using <%= console %> anywhere in the code.
gem 'web-console', '>= 3.3.0'
gem 'listen', '>= 3.0.5', '< 3.2'
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
gem 'spring'
gem 'spring-watcher-listen', '~> 2.0.0'
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

235
Gemfile.lock Normal file
View File

@@ -0,0 +1,235 @@
GEM
remote: http://rubygems.org/
specs:
actioncable (5.1.7)
actionpack (= 5.1.7)
nio4r (~> 2.0)
websocket-driver (~> 0.6.1)
actionmailer (5.1.7)
actionpack (= 5.1.7)
actionview (= 5.1.7)
activejob (= 5.1.7)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.1.7)
actionview (= 5.1.7)
activesupport (= 5.1.7)
rack (~> 2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.1.7)
activesupport (= 5.1.7)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
activejob (5.1.7)
activesupport (= 5.1.7)
globalid (>= 0.3.6)
activemodel (5.1.7)
activesupport (= 5.1.7)
activerecord (5.1.7)
activemodel (= 5.1.7)
activesupport (= 5.1.7)
arel (~> 8.0)
activesupport (5.1.7)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
arel (8.0.0)
base64 (0.3.0)
bcrypt (3.1.21)
bindex (0.8.1)
builder (3.3.0)
byebug (11.1.3)
capybara (2.13.0)
addressable
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
childprocess (4.1.0)
coffee-rails (4.2.2)
coffee-script (>= 2.2.0)
railties (>= 4.0.0)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.12.2)
concurrent-ruby (1.3.6)
crass (1.0.6)
date (3.5.1)
devise (4.9.4)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
erubi (1.13.1)
execjs (2.10.0)
ffi (1.17.3)
globalid (1.1.0)
activesupport (>= 5.0)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
jbuilder (2.13.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
logger (1.7.0)
loofah (2.25.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.9.0)
logger
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
method_source (1.1.0)
mime-types (3.7.0)
logger
mime-types-data (~> 3.2025, >= 3.2025.0507)
mime-types-data (3.2026.0113)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (5.25.4)
net-imap (0.3.9)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.5.1)
net-protocol
nio4r (2.7.5)
nokogiri (1.13.10-x86_64-linux)
racc (~> 1.4)
orm_adapter (0.5.0)
public_suffix (5.1.1)
puma (3.12.6)
racc (1.8.1)
rack (2.2.21)
rack-test (2.2.0)
rack (>= 1.3)
rails (5.1.7)
actioncable (= 5.1.7)
actionmailer (= 5.1.7)
actionpack (= 5.1.7)
actionview (= 5.1.7)
activejob (= 5.1.7)
activemodel (= 5.1.7)
activerecord (= 5.1.7)
activesupport (= 5.1.7)
bundler (>= 1.3.0)
railties (= 5.1.7)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.5.0)
loofah (~> 2.19, >= 2.19.1)
railties (5.1.7)
actionpack (= 5.1.7)
activesupport (= 5.1.7)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rake (13.3.1)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
responders (3.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
rexml (3.4.4)
ruby_dep (1.5.0)
rubyzip (2.4.1)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sass-rails (5.0.7)
railties (>= 4.0.0, < 6)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3)
selenium-webdriver (4.1.0)
childprocess (>= 0.5, < 5.0)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2)
spring (2.1.1)
spring-watcher-listen (2.0.1)
listen (>= 2.7, < 4.0)
spring (>= 1.2, < 3.0)
sprockets (3.7.5)
base64
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.2)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sqlite3 (1.6.9)
mini_portile2 (~> 2.8.0)
thor (1.5.0)
thread_safe (0.3.6)
tilt (2.7.0)
timeout (0.6.0)
turbolinks (5.2.1)
turbolinks-source (~> 5.2)
turbolinks-source (5.2.0)
tzinfo (1.2.11)
thread_safe (~> 0.1)
uglifier (4.2.1)
execjs (>= 0.3.0, < 3)
warden (1.2.9)
rack (>= 2.0.9)
web-console (3.7.0)
actionview (>= 5.0)
activemodel (>= 5.0)
bindex (>= 0.4.0)
railties (>= 5.0)
websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (2.1.0)
nokogiri (~> 1.3)
PLATFORMS
x86_64-linux
DEPENDENCIES
bcrypt (~> 3.1.7)
byebug
capybara (~> 2.13.0)
coffee-rails (~> 4.2)
devise (~> 4.7)
jbuilder (~> 2.5)
listen (>= 3.0.5, < 3.2)
puma (~> 3.7)
rails (~> 5.1.0)
sass-rails (~> 5.0)
selenium-webdriver
spring
spring-watcher-listen (~> 2.0.0)
sqlite3
turbolinks (~> 5)
tzinfo-data
uglifier (>= 1.3.0)
web-console (>= 3.3.0)
BUNDLED WITH
2.4.22

View File

@@ -1,3 +1,3 @@
# PMS
Project Management System
Project Management System

6
Rakefile Normal file
View File

@@ -0,0 +1,6 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require_relative 'config/application'
Rails.application.load_tasks

View File

@@ -0,0 +1,3 @@
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css

0
app/assets/images/.keep Normal file
View File

View 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 .

View 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);

View File

View 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/

View 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

File diff suppressed because it is too large Load Diff

View 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/

View 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/

View File

@@ -0,0 +1,4 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

View File

@@ -0,0 +1,4 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
end

View File

@@ -0,0 +1,20 @@
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include AuthorizationConcern
before_action :authenticate_user!
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

View File

@@ -0,0 +1,31 @@
class CommentsController < ApplicationController
before_action :set_task
def create
authorize_task!
@comment = @task.comments.build(comment_params)
@comment.user = current_user
if @comment.save
respond_to do |format|
format.html { redirect_to @task, notice: 'Comment added.' }
format.js
end
else
respond_to do |format|
format.html { redirect_to @task, alert: 'Comment cannot be blank.' }
format.js
end
end
end
private
def set_task
@task = Task.find(params[:task_id])
end
def comment_params
params.require(:comment).permit(:content)
end
end

View File

View 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

View 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

View File

@@ -0,0 +1,122 @@
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
@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
updated_params = task_params.to_h
if updated_params[:status] == "1"
updated_params[:status] = "completed"
elsif updated_params[:status] == "0"
updated_params[:status] = "open"
end
if @task.update(updated_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

View File

@@ -0,0 +1,2 @@
module ApplicationHelper
end

View File

@@ -0,0 +1,2 @@
module DepartmentsHelper
end

View File

@@ -0,0 +1,2 @@
module TasksHelper
end

View File

@@ -0,0 +1,2 @@
class ApplicationJob < ActiveJob::Base
end

View File

@@ -0,0 +1,4 @@
class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer'
end

View File

@@ -0,0 +1,3 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end

6
app/models/comment.rb Normal file
View File

@@ -0,0 +1,6 @@
class Comment < ApplicationRecord
belongs_to :user
belongs_to :task
validates :content, presence: true
end

View File

20
app/models/department.rb Normal file
View 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

106
app/models/task.rb Normal file
View File

@@ -0,0 +1,106 @@
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
has_many :comments, 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

View File

@@ -0,0 +1,4 @@
class TaskActivity < ApplicationRecord
belongs_to :task
belongs_to :user
end

46
app/models/user.rb Normal file
View File

@@ -0,0 +1,46 @@
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'
has_many :comments, dependent: :destroy
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

View File

@@ -0,0 +1,9 @@
<div class="comment" id="comment_<%= comment.id %>">
<div class="comment-header">
<span class="comment-user"><%= comment.user.name %></span>
<span class="comment-date"><%= time_ago_in_words(comment.created_at) %> ago</span>
</div>
<div class="comment-content">
<%= simple_format(comment.content) %>
</div>
</div>

View File

@@ -0,0 +1,8 @@
<%= form_with(model: [task, comment], local: false, class: "comment-form") do |f| %>
<div class="field">
<%= f.text_area :content, placeholder: "Add a comment...", class: "comment-textarea", required: true %>
</div>
<div class="actions">
<%= f.submit "Post Comment", class: "submit-btn" %>
</div>
<% end %>

View File

@@ -0,0 +1,16 @@
var commentsList = document.getElementById('comments-list');
var commentForm = document.querySelector('.comment-form');
if (commentsList && <%= @comment.persisted? %>) {
var newComment = '<%= j render partial: "comments/comment", locals: { comment: @comment } %>';
commentsList.insertAdjacentHTML('afterbegin', newComment);
commentForm.reset();
// Optional: add a small animation
var commentElement = document.getElementById('comment_<%= @comment.id %>');
commentElement.style.opacity = '0';
commentElement.style.transition = 'opacity 0.5s';
setTimeout(function() {
commentElement.style.opacity = '1';
}, 10);
}

View File

@@ -0,0 +1,2 @@
<h1>Departments#create</h1>
<p>Find me in app/views/departments/create.html.erb</p>

View File

@@ -0,0 +1,2 @@
<h1>Departments#destroy</h1>
<p>Find me in app/views/departments/destroy.html.erb</p>

View 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 %>

View File

@@ -0,0 +1,50 @@
<div class="dashboard-header">
<h1>Departments</h1>
<% if current_user&.admin? %>
<%= link_to 'New Department', new_department_path, class: 'btn btn-success' %>
<% end %>
</div>
<% if flash[:notice] %>
<div class="notice"><%= flash[:notice] %></div>
<% end %>
<div class="content-container">
<div class="departments-grid">
<% if @departments.empty? %>
<div class="empty-state-card">
<p class="empty-state">No departments found.</p>
</div>
<% else %>
<% @departments.each do |department| %>
<div class="department-card-modern">
<div class="department-card-header">
<h3><%= link_to department.name, department_path(department) %></h3>
<div class="department-actions-mini">
<% if current_user&.admin? %>
<%= link_to 'Edit', edit_department_path(department), class: 'btn-mini' %>
<% end %>
</div>
</div>
<p class="department-desc"><%= department.description&.truncate(100) || "No description provided." %></p>
<div class="department-stats-grid">
<div class="dept-stat-box">
<span class="dept-stat-label">Users</span>
<span class="dept-stat-value"><%= department.user_count %></span>
</div>
<div class="dept-stat-box">
<span class="dept-stat-label">Tasks</span>
<span class="dept-stat-value"><%= department.task_count %></span>
</div>
<div class="dept-stat-box">
<span class="dept-stat-label">Open</span>
<span class="dept-stat-value text-primary"><%= department.incomplete_task_count %></span>
</div>
</div>
</div>
<% end %>
<% end %>
</div>
</div>

View 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 %>

View 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>

View File

@@ -0,0 +1,2 @@
<h1>Departments#update</h1>
<p>Find me in app/views/departments/update.html.erb</p>

View 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" %>

View File

@@ -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>

View 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 %>

View 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>

View File

@@ -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>

View 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>

View 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" %>

View 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" %>

View 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 %>

View 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>

View 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>

View 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 %>

View 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 %>

View 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" %>

View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<title>Project Management System</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= 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>

View 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>

View File

@@ -0,0 +1 @@
<%= yield %>

View 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 %>

View File

@@ -0,0 +1,53 @@
<div id="task_<%= task.id %>" class="task-card <%= task.status %>" data-priority="<%= task.priority %>">
<div class="task-card-header">
<div class="task-card-title-section">
<div class="task-checkbox-wrapper">
<%= form_with(model: task, local: false, method: :patch) do |f| %>
<%= f.check_box :status, { checked: task.status == 'completed', onchange: "this.form.submit();" }, "completed", "open" %>
<% end %>
</div>
<h3 class="task-card-title"><%= task.title %></h3>
<% if task.description.present? %>
<span class="task-desc-indicator" title="Has description">📝</span>
<% end %>
</div>
<div class="task-card-badges">
<span class="badge-priority" style="color: <%= task.priority_color %>; border-color: <%= task.priority_color %>22; background-color: <%= task.priority_color %>11;">
<%= task.priority_icon %> <%= task.priority.humanize %>
</span>
<span class="badge-status" style="background-color: <%= task.status_badge[:color] %>">
<%= task.status_badge[:text] %>
</span>
</div>
</div>
<div class="task-card-body">
<div class="task-card-info-grid">
<div class="info-item">
<span class="info-label">Dept</span>
<span class="info-value"><%= task.department&.name || 'Personal' %></span>
</div>
<div class="info-item">
<span class="info-label">Assignee</span>
<span class="info-value"><%= task.assignee&.name || 'Unassigned' %></span>
</div>
<div class="info-item">
<span class="info-label">Created</span>
<span class="info-value"><%= task.created_at.strftime('%b %d, %Y') %></span>
</div>
</div>
</div>
<div class="task-card-footer">
<div class="task-card-actions">
<%= link_to 'View', task, class: 'btn-task-action' %>
<% if task.updateable_by?(current_user) %>
<%= link_to 'Edit', edit_task_path(task), class: 'btn-task-action' %>
<% end %>
<% if task.updateable_by?(current_user) || current_user.admin? %>
<%= link_to 'Delete', task, method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn-task-action action-delete' %>
<% end %>
</div>
</div>
</div>

View File

@@ -0,0 +1,2 @@
<h1>Tasks#create</h1>
<p>Find me in app/views/tasks/create.html.erb</p>

View 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: 'btn btn-primary' %>
<%= link_to 'Create New Task', new_task_path, class: 'btn btn-success' %>
<% if current_user.admin? || current_user.manager? %>
<%= link_to 'Manage Departments', departments_path, class: 'btn btn-secondary' %>
<% end %>
</div>
</div>

View File

@@ -0,0 +1,2 @@
<h1>Tasks#destroy</h1>
<p>Find me in app/views/tasks/destroy.html.erb</p>

View 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>

View File

@@ -0,0 +1,80 @@
<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>
<%= form_with(url: tasks_path, method: :get, local: true, class: "combined-filter-form") do |f| %>
<% if accessible_departments.count > 1 %>
<div class="filter-group">
<label>Department</label>
<%= f.select :department_id,
options_for_select([[ "All Departments", "" ]] + accessible_departments.map { |d| [d.name, d.id] },
params[:department_id]),
{},
{ class: "filter-select", onchange: "this.form.submit();" } %>
</div>
<% end %>
<div class="filter-group">
<label>Priority</label>
<%= f.select :priority,
options_for_select([[ "All Priorities", "" ]] + Task.priorities.keys.map { |p| [p.humanize, p] },
params[:priority]),
{},
{ class: "filter-select", onchange: "this.form.submit();" } %>
</div>
<div class="filter-group">
<label>Status</label>
<%= f.select :status,
options_for_select([[ "All Statuses", "" ]] + Task.statuses.keys.map { |s| [s.humanize, s] },
params[:status]),
{},
{ class: "filter-select", onchange: "this.form.submit();" } %>
</div>
<% 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>

View File

@@ -0,0 +1,50 @@
<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="comments-section">
<h2>Conversations</h2>
<%= render 'comments/form', task: @task, comment: Comment.new %>
<div id="comments-list">
<%= render partial: 'comments/comment', collection: @task.comments.order(created_at: :desc) %>
</div>
</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>

View File

@@ -0,0 +1,2 @@
<h1>Tasks#update</h1>
<p>Find me in app/views/tasks/update.html.erb</p>

View 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 %>';
}

3
bin/bundle Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env ruby
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
load Gem.bin_path('bundler', 'bundle')

4
bin/rails Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'

4
bin/rake Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env ruby
require_relative '../config/boot'
require 'rake'
Rake.application.run

38
bin/setup Executable file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env ruby
require 'pathname'
require 'fileutils'
include FileUtils
# path to your application root.
APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
def system!(*args)
system(*args) || abort("\n== Command #{args} failed ==")
end
chdir APP_ROOT do
# This script is a starting point to setup your application.
# Add necessary setup steps to this file.
puts '== Installing dependencies =='
system! 'gem install bundler --conservative'
system('bundle check') || system!('bundle install')
# Install JavaScript dependencies if using Yarn
# system('bin/yarn')
# puts "\n== Copying sample files =="
# unless File.exist?('config/database.yml')
# cp 'config/database.yml.sample', 'config/database.yml'
# end
puts "\n== Preparing database =="
system! 'bin/rails db:setup'
puts "\n== Removing old logs and tempfiles =="
system! 'bin/rails log:clear tmp:clear'
puts "\n== Restarting application server =="
system! 'bin/rails restart'
end

29
bin/update Executable file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env ruby
require 'pathname'
require 'fileutils'
include FileUtils
# path to your application root.
APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
def system!(*args)
system(*args) || abort("\n== Command #{args} failed ==")
end
chdir APP_ROOT do
# This script is a way to update your development environment automatically.
# Add necessary update steps to this file.
puts '== Installing dependencies =='
system! 'gem install bundler --conservative'
system('bundle check') || system!('bundle install')
puts "\n== Updating database =="
system! 'bin/rails db:migrate'
puts "\n== Removing old logs and tempfiles =="
system! 'bin/rails log:clear tmp:clear'
puts "\n== Restarting application server =="
system! 'bin/rails restart'
end

11
bin/yarn Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env ruby
VENDOR_PATH = File.expand_path('..', __dir__)
Dir.chdir(VENDOR_PATH) do
begin
exec "yarnpkg #{ARGV.join(" ")}"
rescue Errno::ENOENT
$stderr.puts "Yarn executable was not detected in the system."
$stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
exit 1
end
end

26
cloudbuild.yaml Normal file
View File

@@ -0,0 +1,26 @@
steps:
# Build the container image
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', 'gcr.io/$PROJECT_ID/todo-app', '.']
# Push the container image to Container Registry
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/$PROJECT_ID/todo-app']
# Deploy container image to Cloud Run
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
entrypoint: gcloud
args:
- 'run'
- 'deploy'
- 'todo-app'
- '--image'
- 'gcr.io/$PROJECT_ID/todo-app'
- '--region'
- 'us-central1'
- '--platform'
- 'managed'
- '--allow-unauthenticated'
images:
- 'gcr.io/$PROJECT_ID/todo-app'

5
config.ru Normal file
View File

@@ -0,0 +1,5 @@
# This file is used by Rack-based servers to start the application.
require_relative 'config/environment'
run Rails.application

18
config/application.rb Normal file
View File

@@ -0,0 +1,18 @@
require_relative 'boot'
require 'rails/all'
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module Todo
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 5.1
# Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.
end
end

3
config/boot.rb Normal file
View File

@@ -0,0 +1,3 @@
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup' # Set up gems listed in the Gemfile.

10
config/cable.yml Normal file
View File

@@ -0,0 +1,10 @@
development:
adapter: async
test:
adapter: async
production:
adapter: redis
url: redis://localhost:6379/1
channel_prefix: todo_production

22
config/database.yml Normal file
View File

@@ -0,0 +1,22 @@
default: &default
adapter: postgresql
encoding: unicode
# For details on connection pooling, see Rails configuration guide
# http://guides.rubyonrails.org/configuring.html#database-pooling
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
development:
<<: *default
database: todo_development
test:
<<: *default
database: todo_test
production:
<<: *default
database: <%= ENV['DB_NAME'] || 'todo_production' %>
username: <%= ENV['DB_USER'] %>
password: <%= ENV['DB_PASSWORD'] %>
host: <%= ENV['DB_HOST'] %>
# When using Cloud SQL with Cloud Run, the host is often /cloudsql/CONNECTION_NAME

5
config/environment.rb Normal file
View File

@@ -0,0 +1,5 @@
# Load the Rails application.
require_relative 'application'
# Initialize the Rails application.
Rails.application.initialize!

View File

@@ -0,0 +1,57 @@
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# In the development environment your application's code is reloaded on
# every request. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
config.cache_classes = false
# Do not eager load code on boot.
config.eager_load = false
# Show full error reports.
config.consider_all_requests_local = true
# Enable/disable caching. By default caching is disabled.
if Rails.root.join('tmp/caching-dev.txt').exist?
config.action_controller.perform_caching = true
config.cache_store = :memory_store
config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}"
}
else
config.action_controller.perform_caching = false
config.cache_store = :null_store
end
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
config.action_mailer.perform_caching = false
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
# Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load
# Debug mode disables concatenation and preprocessing of assets.
# This option may cause significant delays in view rendering with a large
# number of complex assets.
config.assets.debug = true
# Suppress logger output for asset requests.
config.assets.quiet = true
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
# Configure default URL options for mailers
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
# Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem.
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
end

View File

@@ -0,0 +1,91 @@
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# Code is not reloaded between requests.
config.cache_classes = true
# Eager load code on boot. This eager loads most of Rails and
# your application in memory, allowing both threaded web servers
# and those relying on copy on write to perform better.
# Rake tasks automatically ignore this option for performance.
config.eager_load = true
# Full error reports are disabled and caching is turned on.
config.consider_all_requests_local = false
config.action_controller.perform_caching = true
# Attempt to read encrypted secrets from `config/secrets.yml.enc`.
# Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or
# `config/secrets.yml.key`.
config.read_encrypted_secrets = true
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
# Compress JavaScripts and CSS.
config.assets.js_compressor = :uglifier
# config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
# `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.action_controller.asset_host = 'http://assets.example.com'
# Specifies the header that your server uses for sending files.
# config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
# Mount Action Cable outside main process or domain
# config.action_cable.mount_path = nil
# config.action_cable.url = 'wss://example.com/cable'
# config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
# Use the lowest log level to ensure availability of diagnostic information
# when problems arise.
config.log_level = :debug
# Prepend all log lines with the following tags.
config.log_tags = [ :request_id ]
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
# Use a real queuing backend for Active Job (and separate queues per environment)
# config.active_job.queue_adapter = :resque
# config.active_job.queue_name_prefix = "todo_#{Rails.env}"
config.action_mailer.perform_caching = false
# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
# Send deprecation notices to registered listeners.
config.active_support.deprecation = :notify
# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new
# Use a different logger for distributed setups.
# require 'syslog/logger'
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
if ENV["RAILS_LOG_TO_STDOUT"].present?
logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger)
end
# Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false
end

View File

@@ -0,0 +1,42 @@
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
config.cache_classes = true
# Do not eager load code on boot. This avoids loading your whole application
# just for the purpose of running a single test. If you are using a tool that
# preloads Rails for running tests, you may have to set it to true.
config.eager_load = false
# Configure public file server for tests with Cache-Control for performance.
config.public_file_server.enabled = true
config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}"
}
# Show full error reports and disable caching.
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
# Raise exceptions instead of rendering exception templates.
config.action_dispatch.show_exceptions = false
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
config.action_mailer.perform_caching = false
# Tell Action Mailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
end

View File

@@ -0,0 +1,6 @@
# Be sure to restart your server when you modify this file.
# ApplicationController.renderer.defaults.merge!(
# http_host: 'example.org',
# https: false
# )

View File

@@ -0,0 +1,14 @@
# Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = '1.0'
# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
# Add Yarn node_modules folder to the asset load path.
Rails.application.config.assets.paths << Rails.root.join('node_modules')
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in the app/assets
# folder are already added.
# Rails.application.config.assets.precompile += %w( admin.js admin.css )

View File

@@ -0,0 +1,7 @@
# Be sure to restart your server when you modify this file.
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
# Rails.backtrace_cleaner.remove_silencers!

View File

@@ -0,0 +1,5 @@
# Be sure to restart your server when you modify this file.
# Specify a serializer for the signed and encrypted cookie jars.
# Valid options are :json, :marshal, and :hybrid.
Rails.application.config.action_dispatch.cookies_serializer = :json

View File

@@ -0,0 +1,313 @@
# frozen_string_literal: true
# Assuming you have not yet modified this file, each configuration option below
# is set to its default value. Note that some are commented out while others
# are not: uncommented lines are intended to protect your configuration from
# breaking changes in upgrades (i.e., in the event that future versions of
# Devise change the default values for those options).
#
# Use this hook to configure devise mailer, warden hooks and so forth.
# Many of these configuration options can be set straight in your model.
Devise.setup do |config|
# The secret key used by Devise. Devise uses this key to generate
# random tokens. Changing this key will render invalid all existing
# confirmation, reset password and unlock tokens in the database.
# Devise will use the `secret_key_base` as its `secret_key`
# by default. You can change it below and use your own secret key.
# config.secret_key = '1e92a41bc3c71b359257e2f655aa7acea90e5f21d68dc6a5e98997aca9211cbb6a2c3e4ad8a48938e4d028888c88d30628c3da34d04e6b69fc6210cdc12ed0d0'
# ==> Controller configuration
# Configure the parent class to the devise controllers.
# config.parent_controller = 'DeviseController'
# ==> Mailer Configuration
# Configure the e-mail address which will be shown in Devise::Mailer,
# note that it will be overwritten if you use your own mailer class
# with default "from" parameter.
config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
# Configure the class responsible to send e-mails.
# config.mailer = 'Devise::Mailer'
# Configure the parent class responsible to send e-mails.
# config.parent_mailer = 'ActionMailer::Base'
# ==> ORM configuration
# Load and configure the ORM. Supports :active_record (default) and
# :mongoid (bson_ext recommended) by default. Other ORMs may be
# available as additional gems.
require 'devise/orm/active_record'
# ==> Configuration for any authentication mechanism
# Configure which keys are used when authenticating a user. The default is
# just :email. You can configure it to use [:username, :subdomain], so for
# authenticating a user, both parameters are required. Remember that those
# parameters are used only when authenticating and not when retrieving from
# session. If you need permissions, you should implement that in a before filter.
# You can also supply a hash where the value is a boolean determining whether
# or not authentication should be aborted when the value is not present.
# config.authentication_keys = [:email]
# Configure parameters from the request object used for authentication. Each entry
# given should be a request method and it will automatically be passed to the
# find_for_authentication method and considered in your model lookup. For instance,
# if you set :request_keys to [:subdomain], :subdomain will be used on authentication.
# The same considerations mentioned for authentication_keys also apply to request_keys.
# config.request_keys = []
# Configure which authentication keys should be case-insensitive.
# These keys will be downcased upon creating or modifying a user and when used
# to authenticate or find a user. Default is :email.
config.case_insensitive_keys = [:email]
# Configure which authentication keys should have whitespace stripped.
# These keys will have whitespace before and after removed upon creating or
# modifying a user and when used to authenticate or find a user. Default is :email.
config.strip_whitespace_keys = [:email]
# Tell if authentication through request.params is enabled. True by default.
# It can be set to an array that will enable params authentication only for the
# given strategies, for example, `config.params_authenticatable = [:database]` will
# enable it only for database (email + password) authentication.
# config.params_authenticatable = true
# Tell if authentication through HTTP Auth is enabled. False by default.
# It can be set to an array that will enable http authentication only for the
# given strategies, for example, `config.http_authenticatable = [:database]` will
# enable it only for database authentication.
# For API-only applications to support authentication "out-of-the-box", you will likely want to
# enable this with :database unless you are using a custom strategy.
# The supported strategies are:
# :database = Support basic authentication with authentication key + password
# config.http_authenticatable = false
# If 401 status code should be returned for AJAX requests. True by default.
# config.http_authenticatable_on_xhr = true
# The realm used in Http Basic Authentication. 'Application' by default.
# config.http_authentication_realm = 'Application'
# It will change confirmation, password recovery and other workflows
# to behave the same regardless if the e-mail provided was right or wrong.
# Does not affect registerable.
# config.paranoid = true
# By default Devise will store the user in session. You can skip storage for
# particular strategies by setting this option.
# Notice that if you are skipping storage for all authentication paths, you
# may want to disable generating routes to Devise's sessions controller by
# passing skip: :sessions to `devise_for` in your config/routes.rb
config.skip_session_storage = [:http_auth]
# By default, Devise cleans up the CSRF token on authentication to
# avoid CSRF token fixation attacks. This means that, when using AJAX
# requests for sign in and sign up, you need to get a new CSRF token
# from the server. You can disable this option at your own risk.
# config.clean_up_csrf_token_on_authentication = true
# When false, Devise will not attempt to reload routes on eager load.
# This can reduce the time taken to boot the app but if your application
# requires the Devise mappings to be loaded during boot time the application
# won't boot properly.
# config.reload_routes = true
# ==> Configuration for :database_authenticatable
# For bcrypt, this is the cost for hashing the password and defaults to 12. If
# using other algorithms, it sets how many times you want the password to be hashed.
# The number of stretches used for generating the hashed password are stored
# with the hashed password. This allows you to change the stretches without
# invalidating existing passwords.
#
# Limiting the stretches to just one in testing will increase the performance of
# your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use
# a value less than 10 in other environments. Note that, for bcrypt (the default
# algorithm), the cost increases exponentially with the number of stretches (e.g.
# a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation).
config.stretches = Rails.env.test? ? 1 : 12
# Set up a pepper to generate the hashed password.
# config.pepper = '47bd3fc6f385cd4ca20b230d7e7e43e91e595623be574e3d3af0c256b85936d1edf0366db3fd22cda4bcaa95d936da5dbb68367682448a07a0f067f2f258d0df'
# Send a notification to the original email when the user's email is changed.
# config.send_email_changed_notification = false
# Send a notification email when the user's password is changed.
# config.send_password_change_notification = false
# ==> Configuration for :confirmable
# A period that the user is allowed to access the website even without
# confirming their account. For instance, if set to 2.days, the user will be
# able to access the website for two days without confirming their account,
# access will be blocked just in the third day.
# You can also set it to nil, which will allow the user to access the website
# without confirming their account.
# Default is 0.days, meaning the user cannot access the website without
# confirming their account.
# config.allow_unconfirmed_access_for = 2.days
# A period that the user is allowed to confirm their account before their
# token becomes invalid. For example, if set to 3.days, the user can confirm
# their account within 3 days after the mail was sent, but on the fourth day
# their account can't be confirmed with the token any more.
# Default is nil, meaning there is no restriction on how long a user can take
# before confirming their account.
# config.confirm_within = 3.days
# If true, requires any email changes to be confirmed (exactly the same way as
# initial account confirmation) to be applied. Requires additional unconfirmed_email
# db field (see migrations). Until confirmed, new email is stored in
# unconfirmed_email column, and copied to email column on successful confirmation.
config.reconfirmable = true
# Defines which key will be used when confirming an account
# config.confirmation_keys = [:email]
# ==> Configuration for :rememberable
# The time the user will be remembered without asking for credentials again.
# config.remember_for = 2.weeks
# Invalidates all the remember me tokens when the user signs out.
config.expire_all_remember_me_on_sign_out = true
# If true, extends the user's remember period when remembered via cookie.
# config.extend_remember_period = false
# Options to be passed to the created cookie. For instance, you can set
# secure: true in order to force SSL only cookies.
# config.rememberable_options = {}
# ==> Configuration for :validatable
# Range for password length.
config.password_length = 6..128
# Email regex used to validate email formats. It simply asserts that
# one (and only one) @ exists in the given string. This is mainly
# to give user feedback and not to assert the e-mail validity.
config.email_regexp = /\A[^@\s]+@[^@\s]+\z/
# ==> Configuration for :timeoutable
# The time you want to timeout the user session without activity. After this
# time the user will be asked for credentials again. Default is 30 minutes.
# config.timeout_in = 30.minutes
# ==> Configuration for :lockable
# Defines which strategy will be used to lock an account.
# :failed_attempts = Locks an account after a number of failed attempts to sign in.
# :none = No lock strategy. You should handle locking by yourself.
# config.lock_strategy = :failed_attempts
# Defines which key will be used when locking and unlocking an account
# config.unlock_keys = [:email]
# Defines which strategy will be used to unlock an account.
# :email = Sends an unlock link to the user email
# :time = Re-enables login after a certain amount of time (see :unlock_in below)
# :both = Enables both strategies
# :none = No unlock strategy. You should handle unlocking by yourself.
# config.unlock_strategy = :both
# Number of authentication tries before locking an account if lock_strategy
# is failed attempts.
# config.maximum_attempts = 20
# Time interval to unlock the account if :time is enabled as unlock_strategy.
# config.unlock_in = 1.hour
# Warn on the last attempt before the account is locked.
# config.last_attempt_warning = true
# ==> Configuration for :recoverable
#
# Defines which key will be used when recovering the password for an account
# config.reset_password_keys = [:email]
# Time interval you can reset your password with a reset password key.
# Don't put a too small interval or your users won't have the time to
# change their passwords.
config.reset_password_within = 6.hours
# When set to false, does not sign a user in automatically after their password is
# reset. Defaults to true, so a user is signed in automatically after a reset.
# config.sign_in_after_reset_password = true
# ==> Configuration for :encryptable
# Allow you to use another hashing or encryption algorithm besides bcrypt (default).
# You can use :sha1, :sha512 or algorithms from others authentication tools as
# :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20
# for default behavior) and :restful_authentication_sha1 (then you should set
# stretches to 10, and copy REST_AUTH_SITE_KEY to pepper).
#
# Require the `devise-encryptable` gem when using anything other than bcrypt
# config.encryptor = :sha512
# ==> Scopes configuration
# Turn scoped views on. Before rendering "sessions/new", it will first check for
# "users/sessions/new". It's turned off by default because it's slower if you
# are using only default views.
# config.scoped_views = false
# Configure the default scope given to Warden. By default it's the first
# devise role declared in your routes (usually :user).
# config.default_scope = :user
# Set this configuration to false if you want /users/sign_out to sign out
# only the current scope. By default, Devise signs out all scopes.
# config.sign_out_all_scopes = true
# ==> Navigation configuration
# Lists the formats that should be treated as navigational. Formats like
# :html should redirect to the sign in page when the user does not have
# access, but formats like :xml or :json, should return 401.
#
# If you have any extra navigational formats, like :iphone or :mobile, you
# should add them to the navigational formats lists.
#
# The "*/*" below is required to match Internet Explorer requests.
# config.navigational_formats = ['*/*', :html, :turbo_stream]
# The default HTTP method used to sign out a resource. Default is :delete.
config.sign_out_via = :delete
# ==> OmniAuth
# Add a new OmniAuth provider. Check the wiki for more information on setting
# up on your models and hooks.
# config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
# ==> Warden configuration
# If you want to use other strategies, that are not supported by Devise, or
# change the failure app, you can configure them inside the config.warden block.
#
# config.warden do |manager|
# manager.intercept_401 = false
# manager.default_strategies(scope: :user).unshift :some_external_strategy
# end
# ==> Mountable engine configurations
# When using Devise inside an engine, let's call it `MyEngine`, and this engine
# is mountable, there are some extra configurations to be taken into account.
# The following options are available, assuming the engine is mounted as:
#
# mount MyEngine, at: '/my_engine'
#
# The router that invoked `devise_for`, in the example above, would be:
# config.router_name = :my_engine
#
# When using OmniAuth, Devise cannot automatically set OmniAuth path,
# so you need to do it manually. For the users scope, it would be:
# config.omniauth_path_prefix = '/my_engine/users/auth'
# ==> Hotwire/Turbo configuration
# When using Devise with Hotwire/Turbo, the http status for error responses
# and some redirects must match the following. The default in Devise for existing
# apps is `200 OK` and `302 Found` respectively, but new apps are generated with
# these new defaults that match Hotwire/Turbo behavior.
# Note: These might become the new default in future versions of Devise.
config.responder.error_status = :unprocessable_entity
config.responder.redirect_status = :see_other
# ==> Configuration for :registerable
# When set to false, does not sign a user in automatically after their password is
# changed. Defaults to true, so a user is signed in automatically after changing a password.
# config.sign_in_after_change_password = true
end

View File

@@ -0,0 +1,4 @@
# Be sure to restart your server when you modify this file.
# Configure sensitive parameters which will be filtered from the log file.
Rails.application.config.filter_parameters += [:password]

View File

@@ -0,0 +1,16 @@
# Be sure to restart your server when you modify this file.
# Add new inflection rules using the following format. Inflections
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.plural /^(ox)$/i, '\1en'
# inflect.singular /^(ox)en/i, '\1'
# inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep )
# end
# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.acronym 'RESTful'
# end

View File

@@ -0,0 +1,4 @@
# Be sure to restart your server when you modify this file.
# Add new mime types for use in respond_to blocks:
# Mime::Type.register "text/richtext", :rtf

View File

@@ -0,0 +1,14 @@
# Be sure to restart your server when you modify this file.
# This file contains settings for ActionController::ParamsWrapper which
# is enabled by default.
# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
ActiveSupport.on_load(:action_controller) do
wrap_parameters format: [:json]
end
# To enable root element in JSON for ActiveRecord objects.
# ActiveSupport.on_load(:active_record) do
# self.include_root_in_json = true
# end

View File

@@ -0,0 +1,65 @@
# Additional translations at https://github.com/heartcombo/devise/wiki/I18n
en:
devise:
confirmations:
confirmed: "Your email address has been successfully confirmed."
send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
failure:
already_authenticated: "You are already signed in."
inactive: "Your account is not activated yet."
invalid: "Invalid %{authentication_keys} or password."
locked: "Your account is locked."
last_attempt: "You have one more attempt before your account is locked."
not_found_in_database: "Invalid %{authentication_keys} or password."
timeout: "Your session expired. Please sign in again to continue."
unauthenticated: "You need to sign in or sign up before continuing."
unconfirmed: "You have to confirm your email address before continuing."
mailer:
confirmation_instructions:
subject: "Confirmation instructions"
reset_password_instructions:
subject: "Reset password instructions"
unlock_instructions:
subject: "Unlock instructions"
email_changed:
subject: "Email Changed"
password_change:
subject: "Password Changed"
omniauth_callbacks:
failure: "Could not authenticate you from %{kind} because \"%{reason}\"."
success: "Successfully authenticated from %{kind} account."
passwords:
no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
updated: "Your password has been changed successfully. You are now signed in."
updated_not_active: "Your password has been changed successfully."
registrations:
destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."
signed_up: "Welcome! You have signed up successfully."
signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated."
signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked."
signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account."
update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address."
updated: "Your account has been updated successfully."
updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again."
sessions:
signed_in: "Signed in successfully."
signed_out: "Signed out successfully."
already_signed_out: "Signed out successfully."
unlocks:
send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes."
send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes."
unlocked: "Your account has been unlocked successfully. Please sign in to continue."
errors:
messages:
already_confirmed: "was already confirmed, please try signing in"
confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one"
expired: "has expired, please request a new one"
not_found: "not found"
not_locked: "was not locked"
not_saved:
one: "1 error prohibited this %{resource} from being saved:"
other: "%{count} errors prohibited this %{resource} from being saved:"

Some files were not shown because too many files have changed in this diff Show More