diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb index 8d30fcef39e3ce32b16e3aa0c0712e595c97f132..d36730d0a95ddc4963b6bba2594285f193e026d4 100644 --- a/app/controllers/assignments_controller.rb +++ b/app/controllers/assignments_controller.rb @@ -5,7 +5,7 @@ class AssignmentsController < ApplicationController before_action :set_session, :set_users def index - @assignments = Assignment.all.joins(:session, :user).order("sessions.starts_at") + @assignments = Assignment.joins(session: :conference).where(conferences: { active: true }).joins(:user).order("sessions.starts_at") return unless params[:user_id] @assignments = @assignments.where(user_id: params[:user_id]) @@ -74,7 +74,15 @@ class AssignmentsController < ApplicationController end def by_user - @user = User.includes(:assignments).find(params[:user_id]) + @user = User.find(params[:user_id]) + # Filter assignments to only include those from active conferences + @active_assignments = @user.assignments.joins(session: :conference).where(conferences: { active: true }).includes(:session, session: [:conference, :stage]) + + # Include candidates if feature flag is enabled + if include_candidate_sessions? + @active_candidates = @user.candidates.joins(session: :conference).where(conferences: { active: true }).includes(:session, session: [:conference, :stage]) + end + respond_to do |format| format.html # This will render the existing HTML view (assignments/by_user.html.erb) @@ -83,7 +91,8 @@ class AssignmentsController < ApplicationController tz = TZInfo::Timezone.get("UTC") calendar.add_timezone tz.ical_timezone Time.now - @user.assignments.each do |assignment| + # Add confirmed assignments from active conferences only + @user.assignments.joins(session: :conference).where(conferences: { active: true }).each do |assignment| session = assignment.session assignees = session.assignments.map { |a| a.user.name } desc = [ @@ -102,10 +111,45 @@ class AssignmentsController < ApplicationController event.created = Icalendar::Values::DateTime.new(session.created_at) event.last_modified = Icalendar::Values::DateTime.new(session.updated_at) event.uid = [ session.conference.slug, session.ref_id ].join("-") + event.status = "CONFIRMED" event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", desc.join("<hr>")) calendar.add_event(event) end + # Add candidate sessions from active conferences if feature flag is enabled + if include_candidate_sessions? + @user.candidates.joins(session: :conference).where(conferences: { active: true }).includes(:session).each do |candidate| + session = candidate.session + # Skip if user is already assigned to this session + next if @user.assignments.any? { |a| a.session_id == session.id } + + assignees = session.assignments.map { |a| a.user.name } + candidates = session.candidates.map { |c| c.user.name } + desc = [ + "🤔 CANDIDATE SESSION - You've expressed interest in this session", + "Current Assignees: #{assignees.any? ? assignees.join(', ') : 'None yet'}", + "Other Candidates: #{candidates.reject { |name| name == @user.name }.join(', ')}", + "Speakers: #{session.speakers.map(&:name).join(', ')}", + session.description + ] + desc.unshift("Filedrop has data for this session!<br>\n" + conference_session_url(session.conference, session)) if session.filedrop? + + event = Icalendar::Event.new + event.dtstart = Icalendar::Values::DateTime.new(session.starts_at, tzid: session.starts_at.time_zone.tzinfo.name) + event.dtend = Icalendar::Values::DateTime.new(session.ends_at, tzid: session.ends_at.time_zone.tzinfo.name) + event.summary = "[CANDIDATE] #{session.title} @ #{session.stage.name}" + event.description = desc.map { |l| helpers.strip_tags(l) }.join("\n\n") + event.location = [ session.stage.name, session.conference.name ].join(" @ ") + event.created = Icalendar::Values::DateTime.new(session.created_at) + event.last_modified = Icalendar::Values::DateTime.new(session.updated_at) + event.uid = [ session.conference.slug, session.ref_id, "candidate" ].join("-") + event.status = "TENTATIVE" + event.transp = "TRANSPARENT" # Shows as "free" time in most calendar apps + event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", desc.join("<hr>")) + calendar.add_event(event) + end + end + calendar.publish headers["Content-Type"] = "text/calendar; charset=UTF-8" render plain: calendar.to_ical @@ -127,4 +171,11 @@ class AssignmentsController < ApplicationController def set_users @users = User.all end + + def include_candidate_sessions? + # Feature flag to include candidate sessions in iCal feed + # Can be controlled via environment variable or system setting + ENV.fetch("INCLUDE_CANDIDATE_SESSIONS", "false").downcase == "true" || + SystemSetting.include_candidate_sessions? + end end diff --git a/app/models/system_setting.rb b/app/models/system_setting.rb index e0acd291b01afa0ffbf282f18fdcf12a862784d3..6872d3dac0fb2135f2c8ecbd1314272a0704c1c0 100644 --- a/app/models/system_setting.rb +++ b/app/models/system_setting.rb @@ -17,4 +17,20 @@ class SystemSetting < ApplicationRecord setting.save! setting end + + # Get the include candidate sessions setting + def self.include_candidate_sessions? + setting = find_by(key: "include_candidate_sessions") + setting&.value == "true" + end + + # Set the include candidate sessions setting + def self.set_include_candidate_sessions(enabled) + setting = find_or_initialize_by(key: "include_candidate_sessions") + setting.value = enabled.to_s + setting.description = "Include candidate sessions in user iCal feeds as tentative events" + setting.setting_type = "boolean" + setting.save! + setting + end end diff --git a/app/views/assignments/_listview_date.html.erb b/app/views/assignments/_listview_date.html.erb index 058a6327006b9a5d6916c26a403da7c2781c9cd5..62711e2df677579df8034c34fa0106890f23b56d 100644 --- a/app/views/assignments/_listview_date.html.erb +++ b/app/views/assignments/_listview_date.html.erb @@ -1,20 +1,26 @@ <div class="mb-6 <%= Time.parse(date).end_of_day < now ? "past" : "future" %>"> <h3 class="sticky top-0 z-10 text-lg font-semibold text-gray-700 dark:text-gray-300 mb-3 bg-white dark:bg-gray-800 py-2"><%= date %></h3> <ul class="space-y-3"> - <% assignments_on_date.each do |assignment| %> - <li class="<%= assignment.session.starts_at < now ? "past" : "future" %> pl-4 border-l-4 border-gray-200 dark:border-gray-700"> + <% assignments_on_date.each do |item| %> + <% is_candidate = item.is_a?(Candidate) %> + <li class="<%= item.session.starts_at < now ? "past" : "future" %> pl-4 border-l-4 <%= is_candidate ? 'border-yellow-400 dark:border-yellow-600' : 'border-gray-200 dark:border-gray-700' %>"> <div class="flex flex-col md:flex-row md:items-center gap-2"> <span class="tabular-nums font-medium text-gray-900 dark:text-gray-100 min-w-28"> - <%= assignment.session.starts_at.strftime('%H:%M') %> – <%= assignment.session.ends_at.strftime('%H:%M') %> + <%= item.session.starts_at.strftime('%H:%M') %> – <%= item.session.ends_at.strftime('%H:%M') %> </span> <div class="flex-1"> - <%= render partial: 'shared/session_filedrop', locals: { session: assignment.session } %> - <%= link_to assignment.session.title, conference_session_path(assignment.session.conference, assignment.session), class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" %> - <%= render partial: 'shared/session_engelsystem', locals: { session: assignment.session } %> - <span class="text-gray-500 dark:text-gray-400 ml-1">@ <%= assignment.session.stage.name %></span> + <% if is_candidate %> + <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 mr-1"> + CANDIDATE + </span> + <% end %> + <%= render partial: 'shared/session_filedrop', locals: { session: item.session } %> + <%= link_to item.session.title, conference_session_path(item.session.conference, item.session), class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" %> + <%= render partial: 'shared/session_engelsystem', locals: { session: item.session } %> + <span class="text-gray-500 dark:text-gray-400 ml-1">@ <%= item.session.stage.name %></span> </div> <div class="flex items-center space-x-1"> - <% assignment.session.assignments.map(&:user).each do |other_user| %> + <% item.session.assignments.map(&:user).each do |other_user| %> <%= render partial: 'application/user_avatar', locals: { user: other_user } %> <% end %> </div> diff --git a/app/views/assignments/by_user.html.erb b/app/views/assignments/by_user.html.erb index cec5c1084e18b8d4f00bd4f850d63ba4926ba9ad..a707a7402baeb6c8d0ffe5ce19f9f47a11463793 100644 --- a/app/views/assignments/by_user.html.erb +++ b/app/views/assignments/by_user.html.erb @@ -44,8 +44,22 @@ <!-- List View Tab --> <div class="block" id="listViewTab" role="tabpanel" aria-labelledby="list-tab"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 overflow-auto" style="max-height: calc(100vh - 220px);"> - <% @user.assignments.includes(:session, session: :conference).order('sessions.starts_at').group_by { |a| a.session.starts_at.strftime('%Y-%m-%d') }.each do |date, assignments_on_date| %> - <%= render partial: 'listview_date', locals: { assignments_on_date:, date:, now: } %> + <% + # Combine assignments and candidates (excluding duplicates) + all_items = @active_assignments.to_a + if @active_candidates + assigned_session_ids = @active_assignments.map { |a| a.session_id } + @active_candidates.each do |candidate| + unless assigned_session_ids.include?(candidate.session_id) + all_items << candidate + end + end + end + + # Group by date + all_items.sort_by { |item| item.session.starts_at }.group_by { |item| item.session.starts_at.strftime('%Y-%m-%d') }.each do |date, items_on_date| + %> + <%= render partial: 'listview_date', locals: { assignments_on_date: items_on_date, date:, now: } %> <% end %> </div> </div> @@ -66,19 +80,38 @@ </tr> </thead> <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700"> - <% @user.assignments.includes(:session, session: :conference).order('sessions.starts_at').each do |assignment| %> - <tr class="<%= assignment.session.ends_at < Time.now ? "past" : "future" %>"> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= assignment.session.starts_at.strftime('%Y-%m-%d') %></td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= assignment.session.starts_at.strftime('%H:%M') %></td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= assignment.session.ends_at.strftime('%H:%M') %></td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= assignment.session.stage.name %></td> + <% + # Combine assignments and candidates for table view + all_items = @active_assignments.to_a + if @active_candidates + assigned_session_ids = @active_assignments.map { |a| a.session_id } + @active_candidates.each do |candidate| + unless assigned_session_ids.include?(candidate.session_id) + all_items << candidate + end + end + end + + all_items.sort_by { |item| item.session.starts_at }.each do |item| + is_candidate = item.is_a?(Candidate) + %> + <tr class="<%= item.session.ends_at < Time.now ? "past" : "future" %> <%= is_candidate ? 'bg-yellow-50 dark:bg-yellow-900/20' : '' %>"> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= item.session.starts_at.strftime('%Y-%m-%d') %></td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= item.session.starts_at.strftime('%H:%M') %></td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= item.session.ends_at.strftime('%H:%M') %></td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= item.session.stage.name %></td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-200"> - <%= render partial: 'shared/session_filedrop', locals: { session: assignment.session } %> - <%= link_to assignment.session.title, assignment.session.url, target: "_blank", class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" %> - <%= render partial: 'shared/session_engelsystem', locals: { session: assignment.session } %> + <% if is_candidate %> + <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 mr-1"> + CANDIDATE + </span> + <% end %> + <%= render partial: 'shared/session_filedrop', locals: { session: item.session } %> + <%= link_to item.session.title, item.session.url, target: "_blank", class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" %> + <%= render partial: 'shared/session_engelsystem', locals: { session: item.session } %> </td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"> - <% assignment.session.assignments.map(&:user).each do |other_user| %> + <% item.session.assignments.map(&:user).each do |other_user| %> <%= render partial: 'application/user_avatar', locals: { user: other_user } %> <% end %> </td>