diff --git a/app/assets/stylesheets/activity.css b/app/assets/stylesheets/activity.css new file mode 100644 index 00000000..cd59ef0c --- /dev/null +++ b/app/assets/stylesheets/activity.css @@ -0,0 +1,110 @@ +.recent-activity { + margin-top: 1rem; +} + +.recent-activity h3 { + font-size: 1.2rem; + margin-bottom: 0.75rem; + color: var(--muted-color); +} + +.activity-list { + max-height: 600px; + overflow-y: auto; +} + +.activity-item { + padding: 0.75rem 0; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + font-size: 0.9rem; +} + +.activity-item:last-child { + border-bottom: none; +} + +.activity-content { + margin: 0.5rem 0; +} + +.activity-content p { + margin: 0; +} + +.activity-time { + color: var(--muted-color); + font-size: 0.75rem; +} + +.activity-actions { + display: flex; + margin-top: 0.5rem; +} + +.give-kudos-btn { + display: inline-flex; + align-items: center; + background-color: rgba(var(--primary-color-rgb), 0.1); + color: var(--primary-color); + padding: 0.25rem 0.5rem; + border-radius: 4px; + text-decoration: none; + font-size: 0.75rem; + transition: background-color 0.2s; +} + +.give-kudos-btn:hover { + background-color: rgba(var(--primary-color-rgb), 0.2); +} + +.kudos-icon { + margin-right: 0.25rem; + font-style: normal; +} + +.kudos-count { + margin-left: 0.25rem; + background: rgba(var(--primary-color-rgb), 0.2); + border-radius: 10px; + padding: 0.1rem 0.4rem; + font-size: 0.7rem; +} + +.kudos-given, +.kudos-received { + display: inline-flex; + align-items: center; + color: var(--primary-color); + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + background-color: rgba(var(--primary-color-rgb), 0.1); +} + +/* Dark mode overrides */ +@media (prefers-color-scheme: dark) { + .activity-item { + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + } + + .activity-time { + color: rgba(255, 255, 255, 0.5); + } + + .give-kudos-btn { + background-color: rgba(var(--primary-color-rgb), 0.2); + } + + .give-kudos-btn:hover { + background-color: rgba(var(--primary-color-rgb), 0.3); + } + + .kudos-count { + background: rgba(var(--primary-color-rgb), 0.3); + } + + .kudos-given, + .kudos-received { + background-color: rgba(var(--primary-color-rgb), 0.2); + } +} \ No newline at end of file diff --git a/app/controllers/project_milestones_controller.rb b/app/controllers/project_milestones_controller.rb new file mode 100644 index 00000000..996aee23 --- /dev/null +++ b/app/controllers/project_milestones_controller.rb @@ -0,0 +1,31 @@ +class ProjectMilestonesController < ApplicationController + before_action :authenticate_user! + + def give_kudos + milestone = ProjectMilestone.find(params[:id]) + + # Don't allow users to give kudos to themselves + if milestone.user_id == current_user.id + return render json: { error: "You cannot give kudos to yourself" }, status: :unprocessable_entity + end + + # Check if user already gave kudos + if milestone.kudos_from?(current_user.id) + return render json: { error: "You already gave kudos for this milestone" }, status: :unprocessable_entity + end + + kudos = ProjectMilestoneKudos.new( + project_milestone: milestone, + user_id: current_user.id + ) + + if kudos.save + render json: { + success: true, + kudos_count: milestone.reload.kudos_count + } + else + render json: { error: kudos.errors.full_messages.join(", ") }, status: :unprocessable_entity + end + end +end diff --git a/app/javascript/controllers/kudos_controller.js b/app/javascript/controllers/kudos_controller.js new file mode 100644 index 00000000..bb879e71 --- /dev/null +++ b/app/javascript/controllers/kudos_controller.js @@ -0,0 +1,43 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["button"] + + connect() { + console.log("Kudos controller connected") + } + + giveKudos(event) { + event.preventDefault() + + const button = event.currentTarget + const url = button.getAttribute("href") + + fetch(url, { + method: "POST", + headers: { + "X-CSRF-Token": document.querySelector("meta[name='csrf-token']").content, + "Accept": "application/json" + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Replace the button with the kudos count + const container = button.closest(".activity-actions") + + const kudosEl = document.createElement("span") + kudosEl.classList.add("kudos-given") + kudosEl.innerHTML = `👏 ${data.kudos_count}` + + container.innerHTML = "" + container.appendChild(kudosEl) + } else { + console.error("Error giving kudos:", data.error) + } + }) + .catch(error => { + console.error("Error giving kudos:", error) + }) + } +} \ No newline at end of file diff --git a/app/jobs/project_milestone_check_job.rb b/app/jobs/project_milestone_check_job.rb new file mode 100644 index 00000000..6457830c --- /dev/null +++ b/app/jobs/project_milestone_check_job.rb @@ -0,0 +1,50 @@ +class ProjectMilestoneCheckJob < ApplicationJob + queue_as :default + + def perform + # Get all users with heartbeats in the last hour + active_users = Heartbeat.where("created_at > ?", 1.hour.ago) + .distinct.pluck(:user_id) + + active_users.each do |user_id| + check_hourly_milestones(user_id) + end + end + + private + + def check_hourly_milestones(user_id) + user = User.find_by(id: user_id) + return unless user + + # Get projects with significant time in the last period + project_durations = user.heartbeats.today.group(:project).duration_seconds + + project_durations.each do |project, duration| + next if project.blank? + + # Convert to hours + hours = (duration / 3600.0).floor + next if hours < 1 + + # Check if we already have a milestone for this hour count + existing = ProjectMilestone.where( + user_id: user_id, + project_name: project, + milestone_type: :hourly, + milestone_value: hours + ).where("created_at > ?", 1.day.ago).exists? + + # If no milestone exists, create one + unless existing + ProjectMilestone.create!( + user_id: user_id, + project_name: project, + milestone_type: :hourly, + milestone_value: hours + ) + Rails.logger.info "Created hourly milestone for user #{user_id} on project #{project}: #{hours} hours" + end + end + end +end diff --git a/app/models/project_milestone.rb b/app/models/project_milestone.rb new file mode 100644 index 00000000..c33faec3 --- /dev/null +++ b/app/models/project_milestone.rb @@ -0,0 +1,38 @@ +class ProjectMilestone < ApplicationRecord + belongs_to :user, foreign_key: :user_id + + has_many :project_milestone_kudos, class_name: "ProjectMilestoneKudos" + + validates :project_name, presence: true + validates :milestone_type, presence: true + validates :milestone_value, presence: true + + # We keep this because I don't want to change the database schema + enum :milestone_type, { + hourly: 0, + daily: 1, + weekly: 2 + } + + # Get milestones for display in the sidebar + def self.recent_for_display(limit = 20) + where(milestone_type: :hourly) + .order(created_at: :desc) + .includes(:user, :project_milestone_kudos) + .limit(limit) + end + + # Check if the current user has given kudos to this milestone + def kudos_from?(user_id) + project_milestone_kudos.where(user_id: user_id).exists? + end + + # Get the kudos count + def kudos_count + project_milestone_kudos.count + end + + def formatted_message + "completed #{milestone_value} hour#{'s' if milestone_value > 1} on #{project_name}" + end +end diff --git a/app/models/project_milestone_kudos.rb b/app/models/project_milestone_kudos.rb new file mode 100644 index 00000000..7f652fbd --- /dev/null +++ b/app/models/project_milestone_kudos.rb @@ -0,0 +1,6 @@ +class ProjectMilestoneKudos < ApplicationRecord + belongs_to :project_milestone + belongs_to :user + + validates :project_milestone_id, uniqueness: { scope: :user_id } +end diff --git a/app/views/shared/_nav.html.erb b/app/views/shared/_nav.html.erb index ea99a10c..60e3b328 100644 --- a/app/views/shared/_nav.html.erb +++ b/app/views/shared/_nav.html.erb @@ -75,4 +75,44 @@ <% end %> <% end %> + + <% if current_user %> +
<%= milestone.formatted_message %>
+