<%# app/views/admin/timeline/show.html.erb %> <%# Instance variables: @users_with_timeline_data, @primary_user, @date, @next_date, @prev_date %> <% primary_user_tz = @primary_user&.timezone || (current_user&.timezone || 'UTC') timeline_start_hour = 0 timeline_end_hour = 23 user_colors = ['#7C3AED', '#10B981', '#3B82F6', '#F59E0B', '#EF4444', '#DB2777', '#6D28D9'] users_data_array = Array(@users_with_timeline_data) num_users = users_data_array.count num_users = 1 if num_users == 0 # Ensure num_users is at least 1 for calculations # Fixed REM values for layout (assuming 1rem = 16px for px calculations) line_left_rem = 4.0 line_right_rem = 0.5 activity_col_area_start_rem = line_left_rem # Header spacer aligns with hour labels activity_col_area_end_rem = line_right_rem # Header spacer aligns with right padding of grid gutter_rem = 0.25 pixels_per_hour = 128 # Controls vertical scale pixels_per_minute = pixels_per_hour / 60.0 min_column_width_px = 186 # Minimum width for each user column gutter_px = gutter_rem * 16 # Gutter in pixels # Calculate the total minimum width required for all user columns + gutters + fixed label/padding areas # This width will be applied to the admin-timeline-content-sizer div user_columns_total_min_width_px = (num_users * min_column_width_px) gutters_total_width_px = (num_users > 1 ? (num_users - 1) * gutter_px : 0) # Total min width for the content that scrolls (headers part) min_header_content_width_px = user_columns_total_min_width_px + gutters_total_width_px # Total min width for the grid content part (timeline-grid-scroll-container's content) # This area includes the hour labels on the left, then the user activity columns, then right padding min_grid_content_width_px = (line_left_rem * 16) + min_header_content_width_px + (line_right_rem * 16) # The sizer div needs to be at least as wide as the widest of its direct children (header row or grid row) # The header row's actual content part (user headers) spans min_header_content_width_px. # Add fixed spacers for the header: total_min_scroll_width_px = (activity_col_area_start_rem * 16) + min_header_content_width_px + (activity_col_area_end_rem * 16) # Current admin user and selected users for Stimulus current_admin_user = { id: current_user.id, display_name: current_user.display_name, avatar_url: current_user.avatar_url } current_admin_user_json = current_admin_user.to_json initial_selected_users_json = @initial_selected_user_objects.to_json current_date_for_form = @date.to_s %>
<%# Selected user pills will appear here %>
<%= @date.in_time_zone(primary_user_tz).strftime("%A, %B %-d, %Y") %>
<%= link_to "← Prev", admin_timeline_path(date: @prev_date.to_s), class: "button button-outline", data: { "date-nav-link": "true" } %> <%= link_to "Today", admin_timeline_path(date: Time.current.to_date.to_s), class: "button button-outline", data: { "date-nav-link": "true" } %> <%= link_to "Next →", admin_timeline_path(date: @next_date.to_s), class: "button button-outline", data: { "date-nav-link": "true" } %>
<% if users_data_array.any? %>
<% users_data_array.each_with_index do |data, index| %> <% user = data[:user] %> <% total_coded_time_seconds = data[:total_coded_time] %> <% # Calculate the base offset for this column (without the hour label padding) # This ensures evenly spaced columns considering gutters base_offset_px = index * min_column_width_px + (index > 0 ? index * gutter_px : 0) # Add the hour label padding to get the absolute left position - EXACT SAME CALCULATION AS SPANS header_left_px = (line_left_rem * 16) + base_offset_px %>
<%= render "shared/user_mention", user: user %> <% if current_user && user != current_user && user.slack_uid.present? %>
<%= link_to "💬 DM", "slack://user?team=T0266FRGM&id=#{user.slack_uid}", target: "_blank", style: "font-size: 0.7rem; color: #9CA3AF; text-decoration: underline;" %>
<% end %> <% if total_coded_time_seconds && total_coded_time_seconds > 0 %>
<%= short_time_simple(total_coded_time_seconds) %> coded
<% else %>
No time coded
<% end %>
<%= user.timezone %>
<% end %>
<% end %>
<%# Hour markers and lines (background grid) %> <% (timeline_start_hour..timeline_end_hour).each do |hour| %>
<%= Time.utc(2000,1,1, hour).strftime("%-l:00 %p") %>
<%# The actual line is styled to span the container using absolute positioning from CSS %>
<% end %> <%# Current Time Line Indicator %> <% current_time_in_zone = Time.current.in_time_zone(primary_user_tz) is_today = @date == Time.current.in_time_zone(primary_user_tz).to_date show_current_time_line = is_today && current_time_in_zone.hour >= timeline_start_hour && current_time_in_zone.hour < (timeline_end_hour + 1) if show_current_time_line minutes_from_timeline_display_start_for_now = (current_time_in_zone.hour - timeline_start_hour) * 60 + current_time_in_zone.min current_time_line_top_px = (minutes_from_timeline_display_start_for_now * pixels_per_minute) end %> <% if show_current_time_line %>
NOW
<% end %> <%# Logic for calculating span properties (remains mostly the same) %> <% calculate_span_properties = lambda do |span_data, span_user_tz| return nil unless span_data && span_data[:start_time] && span_data[:duration] start_time_in_zone = Time.at(span_data[:start_time]).in_time_zone(span_user_tz) end_time_value = span_data[:end_time] || (span_data[:start_time] + span_data[:duration]) end_time_in_zone = Time.at(end_time_value).in_time_zone(span_user_tz) today_start_of_day_for_span_user = @date.in_time_zone(span_user_tz).beginning_of_day view_start_datetime = today_start_of_day_for_span_user.advance(hours: timeline_start_hour) view_end_datetime = today_start_of_day_for_span_user.advance(hours: timeline_end_hour + 1) effective_start_time = [start_time_in_zone, view_start_datetime].max effective_end_time = [end_time_in_zone, view_end_datetime].min return nil if effective_start_time >= effective_end_time minutes_from_view_start = ((effective_start_time - view_start_datetime) / 60.0).to_f duration_seconds_in_view = effective_end_time - effective_start_time height_px = (duration_seconds_in_view / 60.0) * pixels_per_minute return nil if height_px <= 0.5 final_top_px = (minutes_from_view_start * pixels_per_minute) title_parts = [] title_parts << "Languages: #{span_data[:languages].join(', ')}" if span_data[:languages]&.any? project_title_segments = (span_data[:projects_edited_details] || []).map do |proj_detail| "#{proj_detail[:name]}#{proj_detail[:repo_url] ? ' (GitHub)' : ' (No Repo)'}" end title_parts << "Projects: #{project_title_segments.join('; ')}" if project_title_segments.any? title_parts << "Editors: #{span_data[:editors].join(', ')}" if span_data[:editors]&.any? files_to_show = span_data[:files_edited] || [] if files_to_show.any? max_files_in_tooltip = 5 files_display_string = files_to_show.take(max_files_in_tooltip).join(', ') files_display_string += ", ..." if files_to_show.length > max_files_in_tooltip title_parts << "Files: #{files_display_string}" end title_parts << "Duration: #{Time.at(span_data[:duration]).utc.strftime('%Hh %Mm %Ss')}" title_parts << "Time: #{start_time_in_zone.strftime("%-l:%M %p")} - #{end_time_in_zone.strftime("%-l:%M %p")}" new_title_for_span = title_parts.join("\n") { final_top_px: final_top_px.round(2), height_px: height_px.round(2), title: new_title_for_span, projects_to_display: span_data[:projects_edited_details] || [], display_text_line2: span_data[:languages]&.any? ? span_data[:languages].join(", ") : "-", display_text_line3: "#{start_time_in_zone.strftime("%-l:%M %p")} - #{end_time_in_zone.strftime("%-l:%M %p")}", } end %> <%# Activity Spans %> <% users_data_array.each_with_index do |data, index| %> <% user = data[:user] user_spans = data[:spans] block_color = user_colors[index % user_colors.length] # Calculate the base offset for this column (without the hour label padding) # This ensures evenly spaced columns considering gutters base_offset_px = index * min_column_width_px + (index > 0 ? index * gutter_px : 0) # Add the hour label padding to get the absolute left position current_span_column_left_px = (line_left_rem * 16) + base_offset_px %> <% Array(user_spans).each do |span_data| %> <% props = calculate_span_properties.call(span_data, user.timezone || primary_user_tz) %> <% next unless props %>
<% if props[:projects_to_display]&.any? %> <% props[:projects_to_display].each_with_index do |project_detail, p_idx| %> <% if project_detail[:repo_url].present? %> <%= project_detail[:name].truncate(20) %> <% else %> <%= project_detail[:name].truncate(20) %> 🚫 <% end %> <% if p_idx < props[:projects_to_display].length - 1 && props[:height_px] > 20 %> <%= " / " %> <% end %> <% end %> <% else %> Coding Activity <% end %>
<%= props[:display_text_line2] %>
<%= props[:display_text_line3] %>
<% end %> <% end %>
<% content_for :head do %> <% end %>