Move github source to private server

This commit is contained in:
Zin Bo Thit
2026-01-29 12:00:13 +06:30
parent 417600b23e
commit 87cc77e0fb
27 changed files with 916 additions and 381 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ class ApplicationController < ActionController::Base
include AuthorizationConcern
before_action :authenticate_user!
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :set_current_user_department

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

@@ -53,7 +53,6 @@ class TasksController < ApplicationController
end
def edit
authorize_task_update!(@task)
@departments = accessible_departments.ordered
end
@@ -72,7 +71,14 @@ class TasksController < ApplicationController
end
def update
if @task.update(task_params)
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

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

@@ -4,6 +4,7 @@ class Task < ApplicationRecord
belongs_to :creator, class_name: 'User'
has_many :task_activities, dependent: :destroy
has_many :comments, dependent: :destroy
enum priority: {
planning: 'planning',

View File

@@ -7,6 +7,7 @@ class User < ApplicationRecord
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' }

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

@@ -1,36 +1,50 @@
<h1>Departments</h1>
<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="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 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>
<div class="stat">
<strong>Tasks:</strong> <%= department.task_count %>
</div>
<div class="stat">
<strong>Open:</strong> <%= department.incomplete_task_count %>
<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>
</div>
<% end %>
<% end %>
<% end %>
</div>
<div class="actions-section">
<% if current_user&.admin? %>
<%= link_to 'New Department', new_department_path, class: 'action-btn primary' %>
<% end %>
</div>
</div>

View File

@@ -2,6 +2,8 @@
<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' %>

View File

@@ -1,58 +1,53 @@
<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>
<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>
<% 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 %>
<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>
<% end %>
<span class="created-tag">
Created: <%= task.created_at.strftime('%m/%d/%Y') %>
</span>
<span class="badge-status" style="background-color: <%= task.status_badge[:color] %>">
<%= task.status_badge[:text] %>
</span>
</div>
</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 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

@@ -119,10 +119,10 @@
</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' %>
<%= 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: 'action-btn secondary' %>
<%= link_to 'Manage Departments', departments_path, class: 'btn btn-secondary' %>
<% end %>
</div>
</div>

View File

@@ -21,52 +21,38 @@
<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| %>
<%= 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]),
{ 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| %>
{},
{ 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]),
{ 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| %>
{},
{ 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]),
{ 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' %>
{},
{ class: "filter-select", onchange: "this.form.submit();" } %>
</div>
<% end %>
</div>
</div>
<div class="main-content">

View File

@@ -24,6 +24,15 @@
<% 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 %>