diff --git a/app/controllers/admin/standby_blocks_controller.rb b/app/controllers/admin/standby_blocks_controller.rb index 49f037a225789cb58437facd759b50cf9d92f31f..6f011b92a9c902e63bd9ff8f000f77826f7f536f 100644 --- a/app/controllers/admin/standby_blocks_controller.rb +++ b/app/controllers/admin/standby_blocks_controller.rb @@ -1,12 +1,11 @@ class Admin::StandbyBlocksController < Admin::BaseController before_action :set_conference - before_action :set_standby_block, only: [:show, :edit, :update, :destroy] + before_action :set_standby_block, only: [ :show, :edit, :update, :destroy ] before_action :authorize_permission def index - @standby_blocks = @conference.standby_blocks.includes(:user).order(:starts_at) + @standby_blocks = @conference.standby_blocks.includes(:standby_assignments, :users).order(:starts_at) @suggestions = @conference.suggest_standby_blocks - @users = User.all.order(:name) end def show @@ -14,8 +13,7 @@ class Admin::StandbyBlocksController < Admin::BaseController def new @standby_block = @conference.standby_blocks.build - @users = User.all.order(:name) - + # Pre-fill with suggestion if provided if params[:suggestion_id].present? suggestion = @conference.suggest_standby_blocks[params[:suggestion_id].to_i] @@ -28,57 +26,53 @@ class Admin::StandbyBlocksController < Admin::BaseController def create @standby_block = @conference.standby_blocks.build(standby_block_params) - + if @standby_block.save - redirect_to admin_conference_standby_blocks_path(@conference), - notice: 'Standby block was successfully created.' + redirect_to admin_conference_standby_blocks_path(@conference), + notice: "Standby block was successfully created." else - @users = User.all.order(:name) render :new, status: :unprocessable_entity end end def edit - @users = User.all.order(:name) end def update if @standby_block.update(standby_block_params) - redirect_to admin_conference_standby_blocks_path(@conference), - notice: 'Standby block was successfully updated.' + redirect_to admin_conference_standby_blocks_path(@conference), + notice: "Standby block was successfully updated." else - @users = User.all.order(:name) render :edit, status: :unprocessable_entity end end def destroy @standby_block.destroy - redirect_to admin_conference_standby_blocks_path(@conference), - notice: 'Standby block was successfully deleted.' + redirect_to admin_conference_standby_blocks_path(@conference), + notice: "Standby block was successfully deleted." end def create_from_suggestion suggestion = @conference.suggest_standby_blocks[params[:suggestion_id].to_i] - + unless suggestion - redirect_to admin_conference_standby_blocks_path(@conference), - alert: 'Invalid suggestion selected.' + redirect_to admin_conference_standby_blocks_path(@conference), + alert: "Invalid suggestion selected." return end @standby_block = @conference.standby_blocks.build( - user_id: params[:user_id], starts_at: suggestion[:starts_at], ends_at: suggestion[:ends_at], notes: "Auto-created from suggestion for #{suggestion[:date].strftime('%B %d, %Y')}" ) if @standby_block.save - redirect_to admin_conference_standby_blocks_path(@conference), - notice: 'Standby block was successfully created from suggestion.' + redirect_to admin_conference_standby_blocks_path(@conference), + notice: "Standby block was successfully created from suggestion." else - redirect_to admin_conference_standby_blocks_path(@conference), + redirect_to admin_conference_standby_blocks_path(@conference), alert: "Failed to create standby block: #{@standby_block.errors.full_messages.join(', ')}" end end @@ -94,10 +88,10 @@ class Admin::StandbyBlocksController < Admin::BaseController end def standby_block_params - params.require(:standby_block).permit(:user_id, :starts_at, :ends_at, :notes) + params.require(:standby_block).permit(:starts_at, :ends_at, :notes) end def authorize_permission super("manage_assignments") end -end \ No newline at end of file +end diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb index 667deaa323992563ac42a5b81c79b71cfddb47f5..8731d681f7edb2a190897fa09a1d8662c4a32d04 100644 --- a/app/controllers/assignments_controller.rb +++ b/app/controllers/assignments_controller.rb @@ -6,16 +6,16 @@ class AssignmentsController < ApplicationController def index @assignments = Assignment.joins(session: :conference).where(conferences: { active: true }).joins(:user).order("sessions.starts_at") - @standby_blocks = StandbyBlock.joins(:conference).where(conferences: { active: true }).includes(:user, :conference).order(:starts_at) - + @standby_assignments = StandbyAssignment.joins(standby_block: :conference).where(conferences: { active: true }).includes(:user, standby_block: :conference).order("standby_blocks.starts_at") + if params[:user_id] @assignments = @assignments.where(user_id: params[:user_id]) - @standby_blocks = @standby_blocks.where(user_id: params[:user_id]) + @standby_assignments = @standby_assignments.where(user_id: params[:user_id]) end - + # Create a unified collection for rendering - @assignments_and_standby = (@assignments.map { |a| { type: 'assignment', object: a, user: a.user, starts_at: a.session.starts_at } } + - @standby_blocks.map { |sb| { type: 'standby_block', object: sb, user: sb.user, starts_at: sb.starts_at } }) + @assignments_and_standby = (@assignments.map { |a| { type: "assignment", object: a, user: a.user, starts_at: a.session.starts_at } } + + @standby_assignments.map { |sa| { type: "standby_assignment", object: sa, user: sa.user, starts_at: sa.standby_block.starts_at } }) .sort_by { |item| item[:starts_at] } end @@ -84,14 +84,14 @@ class AssignmentsController < ApplicationController def by_user @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]) - - # Filter standby blocks to only include those from active conferences - @active_standby_blocks = @user.standby_blocks.joins(:conference).where(conferences: { active: true }).includes(:conference) + @active_assignments = @user.assignments.joins(session: :conference).where(conferences: { active: true }).includes(:session, session: [ :conference, :stage ]) + + # Filter standby assignments to only include those from active conferences + @active_standby_assignments = @user.standby_assignments.joins(standby_block: :conference).where(conferences: { active: true }).includes(standby_block: :conference) # 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]) + @active_candidates = @user.candidates.joins(session: :conference).where(conferences: { active: true }).includes(:session, session: [ :conference, :stage ]) end respond_to do |format| @@ -161,12 +161,16 @@ class AssignmentsController < ApplicationController end end - # Add standby blocks from active conferences - @user.standby_blocks.joins(:conference).where(conferences: { active: true }).each do |standby_block| + # Add standby assignments from active conferences + @user.standby_assignments.joins(standby_block: :conference).where(conferences: { active: true }).each do |standby_assignment| + standby_block = standby_assignment.standby_block + other_assignees = standby_block.users.where.not(id: @user.id).map(&:name) + desc = [ "🛡️ STANDBY BLOCK - You are on standby for translation needs", "Conference: #{standby_block.conference.name}", - "Notes: #{standby_block.notes}" + ("Other standby translators: #{other_assignees.join(', ')}" if other_assignees.any?), + ("Notes: #{standby_block.notes}" if standby_block.notes.present?) ].compact.reject(&:blank?) event = Icalendar::Event.new @@ -175,8 +179,8 @@ class AssignmentsController < ApplicationController event.summary = "[STANDBY] Standby Block @ #{standby_block.conference.name}" event.description = desc.join("\n\n") event.location = standby_block.conference.name - event.created = Icalendar::Values::DateTime.new(standby_block.created_at) - event.last_modified = Icalendar::Values::DateTime.new(standby_block.updated_at) + event.created = Icalendar::Values::DateTime.new(standby_assignment.created_at) + event.last_modified = Icalendar::Values::DateTime.new(standby_assignment.updated_at) event.uid = [ standby_block.conference.slug, "standby", standby_block.id ].join("-") event.status = "CONFIRMED" event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", desc.join("<hr>")) diff --git a/app/controllers/conferences_controller.rb b/app/controllers/conferences_controller.rb index e7d3fbcff9dcb41c36870e9c655fc6b73bd2e5e6..c4d85928eac49c9edbda2e70dbcae515e1fab5de 100644 --- a/app/controllers/conferences_controller.rb +++ b/app/controllers/conferences_controller.rb @@ -62,6 +62,8 @@ class ConferencesController < ApplicationController @conference = Conference.find_by(slug: params[:slug]) @sessions = @conference.sessions.where.not(starts_at: nil).includes(:stage, :assignments).where(stage: @conference.relevant_stages).order(:starts_at) + @standby_blocks = @conference.standby_blocks.active.includes(:standby_assignments, :users).order(:starts_at) + if params[:date] date = Time.parse(params[:date]) logger.debug(date) @@ -72,7 +74,19 @@ class ConferencesController < ApplicationController ) } @sessions = @sessions.where(starts_at: start_of_day.call(date)..start_of_day.call(date.advance(days: 1))) + @standby_blocks = @standby_blocks.where(starts_at: start_of_day.call(date)..start_of_day.call(date.advance(days: 1))) end + + # Create a virtual "Standby" stage for standby blocks + @virtual_standby_stage = OpenStruct.new( + name: "Standby", + weight: 999, # Put it at the end + id: "virtual-standby" + ) + + # Combine sessions and standby blocks for timeline display + @all_timeline_items = @sessions.to_a + @standby_blocks.to_a + @users = User.all end diff --git a/app/controllers/standby_assignments_controller.rb b/app/controllers/standby_assignments_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..0ab260da6d8117a1b96c3981fdb42dc8fc7319c1 --- /dev/null +++ b/app/controllers/standby_assignments_controller.rb @@ -0,0 +1,70 @@ +class StandbyAssignmentsController < ApplicationController + before_action :set_standby_block + before_action :set_standby_assignment, only: [ :destroy ] + + def create + @standby_assignment = @standby_block.standby_assignments.build(user: current_user) + + if @standby_assignment.save + flash.now[:success] = "You have been assigned to this standby block." + respond_to do |format| + format.turbo_stream do + render turbo_stream: [ + turbo_stream.replace(helpers.dom_id(@standby_block), partial: "standby_blocks/standby_block", locals: { standby_block: @standby_block }), + turbo_stream.update("flash", partial: "shared/flash") + ] + end + format.html { redirect_back(fallback_location: root_path, notice: "You have been assigned to this standby block.") } + end + else + error_message = "Failed to assign you to this standby block: #{@standby_assignment.errors.full_messages.join(', ')}" + flash.now[:alert] = error_message + respond_to do |format| + format.turbo_stream do + render turbo_stream: [ + turbo_stream.replace(helpers.dom_id(@standby_block), partial: "standby_blocks/standby_block", locals: { standby_block: @standby_block }), + turbo_stream.update("flash", partial: "shared/flash") + ], status: :unprocessable_entity + end + format.html { redirect_back(fallback_location: root_path, alert: error_message) } + end + end + end + + def destroy + if @standby_assignment&.destroy + flash.now[:notice] = "You have been removed from this standby block." + respond_to do |format| + format.turbo_stream do + render turbo_stream: [ + turbo_stream.replace(helpers.dom_id(@standby_block), partial: "standby_blocks/standby_block", locals: { standby_block: @standby_block }), + turbo_stream.update("flash", partial: "shared/flash") + ] + end + format.html { redirect_back(fallback_location: root_path, notice: "You have been removed from this standby block.") } + end + else + flash.now[:alert] = "Failed to remove you from this standby block." + respond_to do |format| + format.turbo_stream do + render turbo_stream: [ + turbo_stream.replace(helpers.dom_id(@standby_block), partial: "standby_blocks/standby_block", locals: { standby_block: @standby_block }), + turbo_stream.update("flash", partial: "shared/flash") + ], status: :unprocessable_entity + end + format.html { redirect_back(fallback_location: root_path, alert: "Failed to remove you from this standby block.") } + end + end + end + + private + + def set_standby_block + conference = Conference.find_by(slug: params[:conference_slug]) + @standby_block = conference.standby_blocks.find(params[:standby_block_id]) + end + + def set_standby_assignment + @standby_assignment = @standby_block.standby_assignments.find_by(user: current_user) + end +end diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 3089f9a107b8ee9477a01003c1581e6a4df1eb4a..73c3745e8d92b36e88eabb293300a73c5a6e7e72 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -5,6 +5,7 @@ class Assignment < ApplicationRecord validates :user_id, uniqueness: { scope: :session_id, message: "has already been assigned to this session" } validate :no_overlapping_assignments + validate :no_overlapping_standby_assignments after_create_commit :notify_assignment_created after_destroy_commit :notify_assignment_destroyed @@ -45,4 +46,21 @@ class Assignment < ApplicationRecord def notify_assignment_destroyed ActiveSupport::Notifications.instrument("assignment.destroyed", record: self) end + + def no_overlapping_standby_assignments + return if session.blank? || user.blank? + + # Check for overlapping standby assignments + overlapping_standby = user.standby_assignments + .joins(:standby_block) + .where.not(id: id) + .where( + "standby_blocks.starts_at < ? AND standby_blocks.ends_at > ?", + session.ends_at, session.starts_at + ) + + if overlapping_standby.exists? + errors.add(:base, "This assignment overlaps with a standby assignment.") + end + end end diff --git a/app/models/conference.rb b/app/models/conference.rb index 665179af6d42fae3418ac5bc04132eed08a685fb..665be08aaf3821f678eb1328ca45cc0b8c171b4a 100644 --- a/app/models/conference.rb +++ b/app/models/conference.rb @@ -93,7 +93,7 @@ class Conference < ApplicationRecord # Get sessions for relevant stages on this day day_sessions = sessions.joins(:stage) .where(stage: relevant_stages) - .where('DATE(sessions.starts_at) = ?', day) + .where("DATE(sessions.starts_at) = ?", day) .order(:starts_at) next if day_sessions.empty? @@ -119,7 +119,7 @@ class Conference < ApplicationRecord block_number = 1 while current_time < program_end - block_end = [current_time + suggested_length, program_end].min + block_end = [ current_time + suggested_length, program_end ].min suggestions << { date: day, @@ -127,7 +127,7 @@ class Conference < ApplicationRecord starts_at: current_time, ends_at: block_end, duration_hours: ((block_end - current_time) / 1.hour).round(1), - sessions_count: day_sessions.where('starts_at >= ? AND starts_at < ?', current_time, block_end).count + sessions_count: day_sessions.where("starts_at >= ? AND starts_at < ?", current_time, block_end).count } current_time = block_end diff --git a/app/models/standby_assignment.rb b/app/models/standby_assignment.rb new file mode 100644 index 0000000000000000000000000000000000000000..f625da22f024305b76630416d374ecc0d2fa3c81 --- /dev/null +++ b/app/models/standby_assignment.rb @@ -0,0 +1,58 @@ +class StandbyAssignment < ApplicationRecord + belongs_to :user + belongs_to :standby_block + + validates :user_id, uniqueness: { scope: :standby_block_id, message: "has already been assigned to this standby block" } + + validate :no_overlapping_assignments + validate :no_overlapping_standby_assignments + + after_create_commit :notify_assignment_created + after_destroy_commit :notify_assignment_destroyed + + scope :future, -> { joins(:standby_block).where("standby_blocks.starts_at" => Time.now..) } + + private + + def no_overlapping_assignments + return if standby_block.blank? || user.blank? + + # Check for overlapping regular session assignments + overlapping_assignments = user.assignments + .joins(:session) + .where.not(id: id) + .where( + "sessions.starts_at < ? AND sessions.ends_at > ?", + standby_block.ends_at, standby_block.starts_at + ) + + if overlapping_assignments.exists? + errors.add(:base, "This standby assignment overlaps with a session assignment.") + end + end + + def no_overlapping_standby_assignments + return if standby_block.blank? || user.blank? + + # Check for overlapping standby assignments + overlapping_standby = user.standby_assignments + .joins(:standby_block) + .where.not(id: id) + .where( + "standby_blocks.starts_at < ? AND standby_blocks.ends_at > ?", + standby_block.ends_at, standby_block.starts_at + ) + + if overlapping_standby.exists? + errors.add(:base, "This standby assignment overlaps with another standby assignment.") + end + end + + def notify_assignment_created + ActiveSupport::Notifications.instrument("standby_assignment.created", record: self) + end + + def notify_assignment_destroyed + ActiveSupport::Notifications.instrument("standby_assignment.destroyed", record: self) + end +end diff --git a/app/models/standby_block.rb b/app/models/standby_block.rb index d5c8cf8920df15e40fec88331d737a4e4bae88ec..c8bc3d013be9589cfa4c76cb2f066fb05393b6fb 100644 --- a/app/models/standby_block.rb +++ b/app/models/standby_block.rb @@ -1,22 +1,24 @@ class StandbyBlock < ApplicationRecord - belongs_to :user belongs_to :conference + has_many :standby_assignments, dependent: :destroy + has_many :users, through: :standby_assignments validates :starts_at, :ends_at, presence: true - validate :no_overlap_with_assignments - validate :no_overlap_with_other_standby_blocks validate :within_conference_dates validate :ends_after_starts - scope :active, -> { where(status: 'active') } - scope :current, -> { active.where('starts_at <= ? AND ends_at > ?', Time.current, Time.current) } - scope :upcoming, -> { active.where('starts_at > ?', Time.current).order(:starts_at) } - scope :for_time_range, ->(start_time, end_time) { active.where('starts_at < ? AND ends_at > ?', end_time, start_time) } + scope :active, -> { where(status: "active") } + scope :current, -> { active.where("starts_at <= ? AND ends_at > ?", Time.current, Time.current) } + scope :upcoming, -> { active.where("starts_at > ?", Time.current).order(:starts_at) } + scope :for_time_range, ->(start_time, end_time) { active.where("starts_at < ? AND ends_at > ?", end_time, start_time) } def self.available_users_at(time) - active.where('starts_at <= ? AND ends_at > ?', time, time) - .includes(:user) - .map(&:user) + joins(:standby_assignments, :users) + .active + .where("starts_at <= ? AND ends_at > ?", time, time) + .map(&:users) + .flatten + .uniq end def duration_minutes @@ -31,10 +33,6 @@ class StandbyBlock < ApplicationRecord OpenStruct.new(name: "Standby") end - def assignments - [OpenStruct.new(user: user)] - end - def ref_id "standby-#{id}" end @@ -64,34 +62,19 @@ class StandbyBlock < ApplicationRecord end def status - super || 'active' + super || "active" end - private - - def no_overlap_with_assignments - return if user.blank? || starts_at.blank? || ends_at.blank? - - overlapping = user.assignments.joins(:session) - .where('sessions.starts_at < ? AND sessions.ends_at > ?', ends_at, starts_at) - - if overlapping.exists? - errors.add(:base, "Standby block overlaps with assigned sessions") - end + def needs_translators? + standby_assignments.count < 2 # Similar to sessions needing 2 translators end - def no_overlap_with_other_standby_blocks - return if user.blank? || starts_at.blank? || ends_at.blank? - - overlapping = user.standby_blocks.active - .where.not(id: id) - .where('starts_at < ? AND ends_at > ?', ends_at, starts_at) - - if overlapping.exists? - errors.add(:base, "Standby block overlaps with another standby block") - end + def assigned_users? + standby_assignments.any? end + private + def within_conference_dates return if conference.blank? || starts_at.blank? || ends_at.blank? @@ -107,4 +90,4 @@ class StandbyBlock < ApplicationRecord errors.add(:ends_at, "must be after start time") end end -end \ No newline at end of file +end diff --git a/app/models/user.rb b/app/models/user.rb index ba7e04433b64e5977d8ecabdb848013674da8678..1e92ab4b9eb1dd0630e8e859e9b6217b903eabec 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,7 +2,8 @@ class User < ApplicationRecord devise :database_authenticatable, :registerable, :rememberable has_many :assignments, dependent: :destroy has_many :candidates, dependent: :destroy - has_many :standby_blocks, dependent: :destroy + has_many :standby_assignments, dependent: :destroy + has_many :standby_blocks, through: :standby_assignments has_many :user_roles, dependent: :destroy has_many :roles, through: :user_roles diff --git a/app/views/admin/standby_blocks/_form.html.erb b/app/views/admin/standby_blocks/_form.html.erb index 39e7327c6ec682e466db4a7d481e35f213b7911b..a4f14ef0a02577b094847eb41640e5cba2bac27a 100644 --- a/app/views/admin/standby_blocks/_form.html.erb +++ b/app/views/admin/standby_blocks/_form.html.erb @@ -25,15 +25,6 @@ <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6"> <div class="grid grid-cols-1 gap-6"> - <!-- User Selection --> - <div> - <%= form.label :user_id, "Translator", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> - <%= form.select :user_id, - options_from_collection_for_select(@users, :id, :name, @standby_block.user_id), - { prompt: "Select a translator..." }, - { class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 shadow-sm focus:border-blue-500 focus:ring-blue-500" } %> - </div> - <!-- Date and Time --> <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div> diff --git a/app/views/admin/standby_blocks/edit.html.erb b/app/views/admin/standby_blocks/edit.html.erb index 36f83287ea16ab5d9913f10c3ed3fdef0745d320..b0e8da81e72b36dc3115768466ea9b0992f29344 100644 --- a/app/views/admin/standby_blocks/edit.html.erb +++ b/app/views/admin/standby_blocks/edit.html.erb @@ -27,12 +27,5 @@ </nav> </div> - <div class="mb-8"> - <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-200">Edit Standby Block</h1> - <p class="mt-1 text-sm text-gray-600 dark:text-gray-400"> - Modify the standby block assignment for <%= @standby_block.user.name %>. - </p> - </div> - <%= render 'form' %> </div> \ No newline at end of file diff --git a/app/views/admin/standby_blocks/index.html.erb b/app/views/admin/standby_blocks/index.html.erb index 4be345e7b0d83655b8c574faaf8586ebfb9ec8b2..19eee7a52184e84a603a63e80b64677acae44251 100644 --- a/app/views/admin/standby_blocks/index.html.erb +++ b/app/views/admin/standby_blocks/index.html.erb @@ -46,14 +46,9 @@ <div><strong>Sessions:</strong> <%= suggestion[:sessions_count] %> sessions during this block</div> </div> - <%= form_with url: admin_conference_standby_blocks_path(@conference), method: :post, local: true, class: "flex items-center gap-2" do |f| %> + <%= form_with url: create_from_suggestion_admin_conference_standby_blocks_path(@conference), method: :post, local: true do |f| %> <%= f.hidden_field :suggestion_id, value: index %> - <%= f.select :user_id, options_from_collection_for_select(@users, :id, :name), - { prompt: "Select user..." }, - { class: "text-xs flex-1 rounded border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200" } %> - <%= f.submit "Create", - formaction: create_from_suggestion_admin_conference_standby_blocks_path(@conference), - class: "btn-sm btn-primary text-xs" %> + <%= f.submit "Create Block", class: "btn-sm btn-primary text-xs w-full" %> <% end %> </div> <% end %> @@ -67,7 +62,7 @@ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <thead class="bg-gray-50 dark:bg-gray-700"> <tr> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">User</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Assigned Users</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Date & Time</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Duration</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Status</th> @@ -79,15 +74,17 @@ <% @standby_blocks.each do |standby_block| %> <tr> <td class="px-6 py-4 whitespace-nowrap"> - <div class="flex items-center"> - <div class="flex-shrink-0"> - <%= render partial: 'application/user_avatar', locals: { user: standby_block.user } %> - </div> - <div class="ml-3"> - <div class="text-sm font-medium text-gray-900 dark:text-gray-200"> - <%= standby_block.user.name %> - </div> - </div> + <div class="flex items-center space-x-2"> + <% if standby_block.users.any? %> + <% standby_block.users.each do |user| %> + <div class="flex items-center"> + <%= render partial: 'application/user_avatar', locals: { user: user } %> + <span class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-200"><%= user.name %></span> + </div> + <% end %> + <% else %> + <span class="text-sm text-gray-500 dark:text-gray-400 italic">No one assigned yet</span> + <% end %> </div> </td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-200"> diff --git a/app/views/assignments/_listview_date.html.erb b/app/views/assignments/_listview_date.html.erb index 3e5fc3e0184fa8d47914c98107aa9631a7b24164..846bb525c779f2b3998e897246b4fa313feec65c 100644 --- a/app/views/assignments/_listview_date.html.erb +++ b/app/views/assignments/_listview_date.html.erb @@ -28,8 +28,9 @@ </div> </div> </li> - <% elsif item_data[:type] == 'standby_block' %> - <% standby_block = item_data[:object] %> + <% elsif item_data[:type] == 'standby_assignment' %> + <% standby_assignment = item_data[:object] %> + <% standby_block = standby_assignment.standby_block %> <li class="<%= standby_block.starts_at < now ? "past" : "future" %> pl-4 border-l-4 border-purple-400 dark:border-purple-600"> <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"> @@ -46,7 +47,9 @@ <% end %> </div> <div class="flex items-center space-x-1"> - <%= render partial: 'application/user_avatar', locals: { user: standby_block.user } %> + <% standby_block.users.each do |user| %> + <%= render partial: 'application/user_avatar', locals: { user: user } %> + <% end %> </div> </div> </li> diff --git a/app/views/assignments/by_user.html.erb b/app/views/assignments/by_user.html.erb index e7c6e8550d1e778e0a872ca6be5744243eea3aa4..8d5dd6bdb38d8284a376ad4bde6592e9e4c75a63 100644 --- a/app/views/assignments/by_user.html.erb +++ b/app/views/assignments/by_user.html.erb @@ -64,9 +64,9 @@ all_items << { type: 'assignment', object: assignment, starts_at: assignment.session.starts_at } end - # Add standby blocks - @active_standby_blocks.each do |standby_block| - all_items << { type: 'standby_block', object: standby_block, starts_at: standby_block.starts_at } + # Add standby assignments + @active_standby_assignments.each do |standby_assignment| + all_items << { type: 'standby_assignment', object: standby_assignment, starts_at: standby_assignment.standby_block.starts_at } end # Add candidates (excluding duplicates) @@ -112,9 +112,9 @@ all_items << { type: 'assignment', object: assignment, starts_at: assignment.session.starts_at } end - # Add standby blocks - @active_standby_blocks.each do |standby_block| - all_items << { type: 'standby_block', object: standby_block, starts_at: standby_block.starts_at } + # Add standby assignments + @active_standby_assignments.each do |standby_assignment| + all_items << { type: 'standby_assignment', object: standby_assignment, starts_at: standby_assignment.standby_block.starts_at } end # Add candidates @@ -153,8 +153,9 @@ <% end %> </td> </tr> - <% elsif item_data[:type] == 'standby_block' %> - <% standby_block = item_data[:object] %> + <% elsif item_data[:type] == 'standby_assignment' %> + <% standby_assignment = item_data[:object] %> + <% standby_block = standby_assignment.standby_block %> <tr class="<%= standby_block.ends_at < Time.now ? "past" : "future" %> bg-purple-50 dark:bg-purple-900/20"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= standby_block.starts_at.strftime('%Y-%m-%d') %></td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= standby_block.starts_at.strftime('%H:%M') %></td> @@ -170,7 +171,9 @@ <% end %> </td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"> - <%= render partial: 'application/user_avatar', locals: { user: standby_block.user } %> + <% standby_block.users.each do |user| %> + <%= render partial: 'application/user_avatar', locals: { user: user } %> + <% end %> </td> </tr> <% end %> diff --git a/app/views/conferences/show.html.erb b/app/views/conferences/show.html.erb index 256aa004cd0835aa56cff492493bb62bbee509a2..d37210bff5b515316ebd3d143ef9f51f735fe04b 100644 --- a/app/views/conferences/show.html.erb +++ b/app/views/conferences/show.html.erb @@ -48,10 +48,23 @@ current_time = Time.zone.now.in_time_zone(@conference.time_zone) <% @conference.days.each do |date| @sessions_by_date = @sessions.group_by { |x| x.starts_at.to_date } + @standby_blocks_by_date = @standby_blocks.group_by { |x| x.starts_at.to_date } @sessions_by_date_and_stage = @sessions_by_date.transform_values{ |sessions| sessions.group_by { |s| s.stage } } - next if @sessions_by_date[date].nil? - day_starts_at = @sessions_by_date[date].first.starts_at - day_ends_at = @sessions_by_date[date].last.ends_at + + # Add standby blocks to the stage grouping as a virtual stage + if @standby_blocks_by_date[date]&.any? + @sessions_by_date_and_stage[date] ||= {} + @sessions_by_date_and_stage[date][@virtual_standby_stage] = @standby_blocks_by_date[date] + end + + next if @sessions_by_date[date].nil? && @standby_blocks_by_date[date].nil? + + # Calculate day boundaries including both sessions and standby blocks + all_items_for_day = (@sessions_by_date[date] || []) + (@standby_blocks_by_date[date] || []) + next if all_items_for_day.empty? + + day_starts_at = all_items_for_day.map(&:starts_at).min + day_ends_at = all_items_for_day.map(&:ends_at).max # round to previous interval timeline_starts_at = day_starts_at.advance(minutes: (day_starts_at.min / timeline_granularity.to_f).floor * timeline_granularity * -0.5) # ... , except rounding up to later interval @@ -128,18 +141,22 @@ current_time = Time.zone.now.in_time_zone(@conference.time_zone) <% @sessions_by_date_and_stage[date].keys.sort_by(&:weight).each do |stage| - sessions = @sessions_by_date_and_stage[date][stage] + items = @sessions_by_date_and_stage[date][stage] %> <div class="stage"> <h4 class="sticky bg-white dark:bg-gray-900 bg-opacity-70 dark:bg-opacity-70 w-full z-30"><%= stage.name %></h4> <div class="stage-sessions"> - <% sessions.each do |session| %> + <% items.each do |item| %> <div class="session-holder hover:z-30 h-full" style=" position: absolute; - top: <%= (session.starts_at - timeline_starts_at) / 3600.0 * pixels_per_hour %>px; - --height: <%= (session.ends_at - session.starts_at) / 3600.0 * pixels_per_hour %>px; + top: <%= (item.starts_at - timeline_starts_at) / 3600.0 * pixels_per_hour %>px; + --height: <%= (item.ends_at - item.starts_at) / 3600.0 * pixels_per_hour %>px; "> - <%= render partial: "sessions/session", locals: { session: session } %> + <% if item.is_a?(StandbyBlock) %> + <%= render partial: "standby_blocks/standby_block", locals: { standby_block: item } %> + <% else %> + <%= render partial: "sessions/session", locals: { session: item } %> + <% end %> </div> <% end %> </div> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 14c2b641108a46492ca8b1d14787d9ebf9b98310..cc23a7e79b06428bdf04569ace65e7b9253ec01e 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -253,19 +253,30 @@ </script> <main class="mb-8"> - <% if notice %> - <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4 mx-4 mt-4 - dark:bg-green-900 dark:border-green-600 dark:text-green-300" role="alert"> - <span class="block sm:inline"><%= notice %></span> - </div> - <% end %> + <div id="flash"> + <% if notice %> + <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4 mx-4 mt-4 + dark:bg-green-900 dark:border-green-600 dark:text-green-300" role="alert"> + <span class="block sm:inline"><%= notice %></span> + </div> + <% end %> - <% if alert %> - <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4 mx-4 mt-4 - dark:bg-red-900 dark:border-red-600 dark:text-red-300" role="alert"> - <span class="block sm:inline"><%= alert %></span> - </div> - <% end %> + <% if alert %> + <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4 mx-4 mt-4 + dark:bg-red-900 dark:border-red-600 dark:text-red-300" role="alert"> + <span class="block sm:inline"><%= alert %></span> + </div> + <% end %> + + <% flash.each do |type, message| %> + <% unless [:notice, :alert].include?(type.to_sym) %> + <div class="bg-blue-100 border border-blue-400 text-blue-700 px-4 py-3 rounded relative mb-4 mx-4 mt-4 + dark:bg-blue-900 dark:border-blue-600 dark:text-blue-300" role="alert"> + <span class="block sm:inline"><%= message %></span> + </div> + <% end %> + <% end %> + </div> <%= yield %> </main> diff --git a/app/views/shared/_flash.html.erb b/app/views/shared/_flash.html.erb index ef5b20ca0201cda70d3e07144c82b6dfda08c51b..b546a119319334b278abd07937f43d4993f227d6 100644 --- a/app/views/shared/_flash.html.erb +++ b/app/views/shared/_flash.html.erb @@ -1,10 +1,35 @@ -<div id="flash" class="relative top-0.5 md:top-16 dark:text-black"> - <% flash.each do |type, message| %> - <div class="flash alert alert-<%= type %>"> - <%= message %> - <button type="button" class="close" data-dismiss="alert" aria-label="Close"> - <span aria-hidden="true">×</span> - </button> - </div> +<% if notice %> + <div class="fixed top-4 left-4 right-4 z-50 bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded shadow-lg + dark:bg-green-900 dark:border-green-600 dark:text-green-300" role="alert"> + <span class="block sm:inline"><%= notice %></span> + </div> +<% end %> + +<% if alert %> + <div class="fixed top-4 left-4 right-4 z-50 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded shadow-lg + dark:bg-red-900 dark:border-red-600 dark:text-red-300" role="alert"> + <span class="block sm:inline"><%= alert %></span> + </div> +<% end %> + +<% flash.each do |type, message| %> + <% unless [:notice, :alert].include?(type.to_sym) %> + <% case type.to_s %> + <% when 'success' %> + <div class="fixed top-4 left-4 right-4 z-50 bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded shadow-lg + dark:bg-green-900 dark:border-green-600 dark:text-green-300" role="alert"> + <span class="block sm:inline"><%= message %></span> + </div> + <% when 'error' %> + <div class="fixed top-4 left-4 right-4 z-50 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded shadow-lg + dark:bg-red-900 dark:border-red-600 dark:text-red-300" role="alert"> + <span class="block sm:inline"><%= message %></span> + </div> + <% else %> + <div class="fixed top-4 left-4 right-4 z-50 bg-blue-100 border border-blue-400 text-blue-700 px-4 py-3 rounded shadow-lg + dark:bg-blue-900 dark:border-blue-600 dark:text-blue-300" role="alert"> + <span class="block sm:inline"><%= message %></span> + </div> + <% end %> <% end %> -</div> +<% end %> diff --git a/app/views/standby_blocks/_standby_block.html.erb b/app/views/standby_blocks/_standby_block.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..cfd52feb12e2327b16e48ffe7370e61afce55a56 --- /dev/null +++ b/app/views/standby_blocks/_standby_block.html.erb @@ -0,0 +1,74 @@ +<div id="<%= dom_id(standby_block) %>" class="standby-block border-l-4 border-purple-400 dark:border-purple-600 bg-purple-50 dark:bg-purple-900/20 p-4 rounded-r-lg mb-4"> + <div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4"> + <!-- Standby Block Info --> + <div class="flex-grow"> + <div class="flex items-center gap-2 mb-2"> + <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200"> + <svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path> + </svg> + STANDBY + </span> + <% if standby_block.needs_translators? %> + <span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200"> + More needed + </span> + <% end %> + </div> + + <h3 class="text-lg font-semibold text-purple-900 dark:text-purple-200 mb-1"> + Standby Block + </h3> + + <div class="text-sm text-purple-700 dark:text-purple-300 mb-2"> + <svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path> + </svg> + <%= standby_block.starts_at.strftime("%H:%M") %> - <%= standby_block.ends_at.strftime("%H:%M") %> + <span class="ml-2 text-purple-600 dark:text-purple-400"> + (<%= standby_block.duration_minutes.round %> min) + </span> + </div> + + <% if standby_block.notes.present? %> + <p class="text-sm text-purple-700 dark:text-purple-300 mb-2"> + <%= standby_block.notes %> + </p> + <% end %> + </div> + + <!-- Assigned Users --> + <div class="flex-shrink-0"> + <div class="flex items-center space-x-2 mb-2"> + <% if standby_block.users.any? %> + <% standby_block.users.each do |user| %> + <%= render partial: 'application/user_avatar', locals: { user: user } %> + <% end %> + <% else %> + <span class="text-sm text-purple-600 dark:text-purple-400 italic">No one assigned</span> + <% end %> + </div> + + <!-- Assignment Form --> + <% if user_signed_in? %> + <% current_assignment = standby_block.standby_assignments.find_by(user: current_user) %> + <% if current_assignment %> + <%= form_with model: [standby_block.conference, standby_block, current_assignment], + method: :delete, + data: { turbo_frame: dom_id(standby_block) }, + class: "inline" do |f| %> + <%= f.submit "Leave Standby", + class: "text-xs px-3 py-1 bg-red-100 hover:bg-red-200 dark:bg-red-900 dark:hover:bg-red-800 text-red-700 dark:text-red-300 rounded border transition-colors duration-200" %> + <% end %> + <% else %> + <%= form_with model: [standby_block.conference, standby_block, standby_block.standby_assignments.build], + data: { turbo_frame: dom_id(standby_block) }, + class: "inline" do |f| %> + <%= f.submit "Join Standby", + class: "text-xs px-3 py-1 bg-purple-100 hover:bg-purple-200 dark:bg-purple-900 dark:hover:bg-purple-800 text-purple-700 dark:text-purple-300 rounded border transition-colors duration-200" %> + <% end %> + <% end %> + <% end %> + </div> + </div> +</div> \ No newline at end of file diff --git a/app/views/standby_blocks/_standby_block_timeline.html.erb b/app/views/standby_blocks/_standby_block_timeline.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..b772ca3858b648bd233126c353a48fdaf944bb57 --- /dev/null +++ b/app/views/standby_blocks/_standby_block_timeline.html.erb @@ -0,0 +1,76 @@ +<div id="<%= dom_id(standby_block) %>" class="standby-block-timeline border-2 border-purple-400 dark:border-purple-600 bg-purple-50 dark:bg-purple-900/20 p-2 rounded h-full relative overflow-hidden"> + <div class="flex flex-col h-full"> + <!-- Header --> + <div class="flex items-center justify-between mb-1"> + <span class="inline-flex items-center px-1 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200"> + <svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path> + </svg> + STANDBY + </span> + <% if standby_block.needs_translators? %> + <span class="inline-flex items-center px-1 py-0.5 rounded text-xs font-medium bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200"> + Need more + </span> + <% end %> + </div> + + <!-- Title --> + <h3 class="text-sm font-semibold text-purple-900 dark:text-purple-200 mb-1 leading-tight"> + Standby Block + </h3> + + <!-- Time --> + <div class="text-xs text-purple-700 dark:text-purple-300 mb-2"> + <%= standby_block.starts_at.strftime("%H:%M") %> - <%= standby_block.ends_at.strftime("%H:%M") %> + </div> + + <!-- Notes (truncated) --> + <% if standby_block.notes.present? %> + <p class="text-xs text-purple-700 dark:text-purple-300 mb-2 line-clamp-2"> + <%= truncate(standby_block.notes, length: 50) %> + </p> + <% end %> + + <!-- Assigned Users --> + <div class="flex-grow flex flex-col justify-end"> + <div class="flex items-center justify-between"> + <div class="flex items-center space-x-1"> + <% if standby_block.users.any? %> + <% standby_block.users.first(3).each do |user| %> + <div class="w-6 h-6"> + <%= render partial: 'application/user_avatar', locals: { user: user } %> + </div> + <% end %> + <% if standby_block.users.count > 3 %> + <span class="text-xs text-purple-600 dark:text-purple-400">+<%= standby_block.users.count - 3 %></span> + <% end %> + <% else %> + <span class="text-xs text-purple-600 dark:text-purple-400 italic">Empty</span> + <% end %> + </div> + + <!-- Assignment Button --> + <% if user_signed_in? %> + <% current_assignment = standby_block.standby_assignments.find_by(user: current_user) %> + <% if current_assignment %> + <%= form_with model: [standby_block.conference, standby_block, current_assignment], + method: :delete, + data: { turbo_frame: dom_id(standby_block) }, + class: "inline" do |f| %> + <%= f.submit "Leave", + class: "text-xs px-1 py-0.5 bg-red-100 hover:bg-red-200 dark:bg-red-900 dark:hover:bg-red-800 text-red-700 dark:text-red-300 rounded border transition-colors duration-200" %> + <% end %> + <% else %> + <%= form_with model: [standby_block.conference, standby_block, standby_block.standby_assignments.build], + data: { turbo_frame: dom_id(standby_block) }, + class: "inline" do |f| %> + <%= f.submit "Join", + class: "text-xs px-1 py-0.5 bg-purple-100 hover:bg-purple-200 dark:bg-purple-900 dark:hover:bg-purple-800 text-purple-700 dark:text-purple-300 rounded border transition-colors duration-200" %> + <% end %> + <% end %> + <% end %> + </div> + </div> + </div> +</div> \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 4295bd94d7b55108194e585a1e0aad4ad10f561f..6740c3ea6f30250f102ed03446229347ebce6b6d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -66,6 +66,9 @@ Rails.application.routes.draw do resources :candidates, only: %i[create destroy] delete "candidates", to: "candidates#destroy_self" end + resources :standby_blocks, only: [] do + resources :standby_assignments, only: %i[create destroy] + end resources :speakers, param: :ref_id end diff --git a/db/migrate/20250525110547_create_standby_blocks.rb b/db/migrate/20250525110547_create_standby_blocks.rb index 4a7f7071770c89ddf8cf4cde8461c9e7d98de014..b1d57db39d7e6e86bfcee0028b3dd7e6eccd0a05 100644 --- a/db/migrate/20250525110547_create_standby_blocks.rb +++ b/db/migrate/20250525110547_create_standby_blocks.rb @@ -10,9 +10,9 @@ class CreateStandbyBlocks < ActiveRecord::Migration[8.0] t.timestamps end - - add_index :standby_blocks, [:user_id, :starts_at] - add_index :standby_blocks, [:conference_id, :starts_at] + + add_index :standby_blocks, [ :user_id, :starts_at ] + add_index :standby_blocks, [ :conference_id, :starts_at ] add_index :standby_blocks, :status end end diff --git a/db/migrate/20250525122442_remove_user_from_standby_blocks.rb b/db/migrate/20250525122442_remove_user_from_standby_blocks.rb new file mode 100644 index 0000000000000000000000000000000000000000..735f749ac81488566150dffbbb58de60666241d8 --- /dev/null +++ b/db/migrate/20250525122442_remove_user_from_standby_blocks.rb @@ -0,0 +1,8 @@ +class RemoveUserFromStandbyBlocks < ActiveRecord::Migration[8.0] + def change + remove_foreign_key :standby_blocks, :users + remove_index :standby_blocks, [ :user_id, :starts_at ] + remove_index :standby_blocks, :user_id + remove_column :standby_blocks, :user_id, :integer + end +end diff --git a/db/migrate/20250525122813_create_standby_assignments.rb b/db/migrate/20250525122813_create_standby_assignments.rb new file mode 100644 index 0000000000000000000000000000000000000000..fcaaaa32ee4c9406fe595bb88f8ae440c520881a --- /dev/null +++ b/db/migrate/20250525122813_create_standby_assignments.rb @@ -0,0 +1,12 @@ +class CreateStandbyAssignments < ActiveRecord::Migration[8.0] + def change + create_table :standby_assignments do |t| + t.references :user, null: false, foreign_key: true + t.references :standby_block, null: false, foreign_key: true + + t.timestamps + end + + add_index :standby_assignments, [ :user_id, :standby_block_id ], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index ccb54b34e309fbf56916582c220cb46535a3e743..b7c3f598fe2291dfd4f9e63d498444591252a69a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_05_25_110547) do +ActiveRecord::Schema[8.0].define(version: 2025_05_25_122813) do create_table "assignments", force: :cascade do |t| t.integer "user_id", null: false t.integer "session_id", null: false @@ -360,8 +360,17 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_25_110547) do t.index ["ref_id", "conference_id"], name: "index_stages_on_ref_id_and_conference_id", unique: true end - create_table "standby_blocks", force: :cascade do |t| + create_table "standby_assignments", force: :cascade do |t| t.integer "user_id", null: false + t.integer "standby_block_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["standby_block_id"], name: "index_standby_assignments_on_standby_block_id" + t.index ["user_id", "standby_block_id"], name: "index_standby_assignments_on_user_id_and_standby_block_id", unique: true + t.index ["user_id"], name: "index_standby_assignments_on_user_id" + end + + create_table "standby_blocks", force: :cascade do |t| t.integer "conference_id", null: false t.datetime "starts_at", null: false t.datetime "ends_at", null: false @@ -372,8 +381,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_25_110547) do t.index ["conference_id", "starts_at"], name: "index_standby_blocks_on_conference_id_and_starts_at" t.index ["conference_id"], name: "index_standby_blocks_on_conference_id" t.index ["status"], name: "index_standby_blocks_on_status" - t.index ["user_id", "starts_at"], name: "index_standby_blocks_on_user_id_and_starts_at" - t.index ["user_id"], name: "index_standby_blocks_on_user_id" end create_table "system_settings", force: :cascade do |t| @@ -437,8 +444,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_25_110547) do add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade add_foreign_key "speakers", "conferences" add_foreign_key "stages", "conferences" + add_foreign_key "standby_assignments", "standby_blocks" + add_foreign_key "standby_assignments", "users" add_foreign_key "standby_blocks", "conferences" - add_foreign_key "standby_blocks", "users" add_foreign_key "user_roles", "roles" add_foreign_key "user_roles", "users" end