Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • eh22
  • main
  • update-readme
  • update-rubocop
4 results

Target

Select target project
  • c3lingo/rescheduled
1 result
Select Git revision
  • eh22
  • main
  • update-readme
  • update-rubocop
4 results
Show changes
Showing
with 718 additions and 78 deletions
...@@ -8,7 +8,7 @@ class Conference < ApplicationRecord ...@@ -8,7 +8,7 @@ class Conference < ApplicationRecord
validates :time_zone, presence: true, inclusion: { in: ActiveSupport::TimeZone.all.map(&:name) } validates :time_zone, presence: true, inclusion: { in: ActiveSupport::TimeZone.all.map(&:name) }
has_many :relevant_stage_links, class_name: 'RelevantStage' has_many :relevant_stage_links, class_name: "RelevantStage"
has_many :relevant_stages, through: :relevant_stage_links, source: :stage has_many :relevant_stages, through: :relevant_stage_links, source: :stage
def days def days
...@@ -20,52 +20,64 @@ class Conference < ApplicationRecord ...@@ -20,52 +20,64 @@ class Conference < ApplicationRecord
end end
def starts_at_in_local_time def starts_at_in_local_time
starts_at.in_time_zone(time_zone || 'UTC') starts_at.in_time_zone(time_zone || "UTC")
end end
def ends_at_in_local_time def ends_at_in_local_time
ends_at.in_time_zone(time_zone || 'UTC') ends_at.in_time_zone(time_zone || "UTC")
end end
def schedule_url def schedule_url
data['schedule_url'] data["schedule_url"]
end end
def filedrop_url def filedrop_url
data['filedrop_url'] data["filedrop_url"]
end end
def engelsystem_url def engelsystem_url
data['engelsystem_url'] data["engelsystem_url"]
end end
def heartbeat_url def heartbeat_url
data['heartbeat_url'] data["heartbeat_url"]
end
# Get the required data fields for the import job class
def required_data_fields
return [] if import_job_class.blank?
begin
# Dynamically query the import job class for its required fields
klass = import_job_class.constantize
klass.respond_to?(:required_data_fields) ? klass.required_data_fields : []
rescue NameError => e
Rails.logger.error("Could not find import job class #{import_job_class}: #{e}")
[]
end
end end
def fetch_translation_angel_id def fetch_translation_angel_id
fetch_engelsystem("angeltypes") fetch_engelsystem("angeltypes")
&.find { |t| t['name'] == 'Translation Angel' } &.find { |t| t["name"] == "Translation Angel" }
&.dig('id') &.dig("id")
end end
def fetch_engelsystem(endpoint) def fetch_engelsystem(endpoint)
begin endpoint_url = engelsystem_url + endpoint
endpoint_url = engelsystem_url + endpoint Rails.logger.debug("Querying engelsystem API at #{endpoint_url}")
Rails.logger.debug("Querying engelsystem API at #{endpoint_url}") response = HTTParty.get(
response = HTTParty.get( endpoint_url,
endpoint_url, headers: {
headers: { "Accept" => "application/json",
'Accept' => 'application/json', "x-api-key" => fetch_credential("engelsystem_token")
"x-api-key" => fetch_credential("engelsystem_token") },
}, timeout: 10
timeout: 10 )
) response.success? ? JSON.parse(response.body)["data"] : nil
return response.success? ? JSON.parse(response.body)["data"] : nil rescue StandardError => e
rescue => e Rails.logger.warn("Engelsystem query for #{endpoint} failed: #{e.message}")
Rails.logger.warn("Engelsystem query for #{endpoint} failed: #{e.message}") nil
return nil
end
end end
def compare_engelsystem_shifts(additional_conferences = []) def compare_engelsystem_shifts(additional_conferences = [])
...@@ -73,35 +85,35 @@ class Conference < ApplicationRecord ...@@ -73,35 +85,35 @@ class Conference < ApplicationRecord
return unless data = fetch_engelsystem("angeltypes/#{translation_angel_id}/shifts") return unless data = fetch_engelsystem("angeltypes/#{translation_angel_id}/shifts")
engelsystem_shifts = data.each_with_object({}) do |shift, hash| engelsystem_shifts = data.each_with_object({}) do |shift, hash|
hash[shift['id']] = shift hash[shift["id"]] = shift
&.dig("needed_angel_types") &.dig("needed_angel_types")
&.find{ |t| t["angel_type"]["id"] == translation_angel_id } &.find { |t| t["angel_type"]["id"] == translation_angel_id }
&.dig("entries") &.dig("entries")
&.map{ |t| t["user"]["name"] } &.map { |t| t["user"]["name"] }
end end
Session Session
.where(conference: [self, *additional_conferences]) .where(conference: [ self, *additional_conferences ])
.where.not(engelsystem_id: nil) .where.not(engelsystem_id: nil)
.includes(assignments: :user) .includes(assignments: :user)
.group_by(&:engelsystem_id) .group_by(&:engelsystem_id)
.each do |engelsystem_id, sessions| .each do |engelsystem_id, sessions|
engelsystem_assigned = engelsystem_shifts[engelsystem_id] engelsystem_assigned = engelsystem_shifts[engelsystem_id]
local_assigned = sessions.flat_map(&:assignments).map{|a|a.user.name} local_assigned = sessions.flat_map(&:assignments).map { |a| a.user.name }
only_engelsystem = engelsystem_assigned - local_assigned only_engelsystem = engelsystem_assigned - local_assigned
only_local = local_assigned - engelsystem_assigned only_local = local_assigned - engelsystem_assigned
unless only_engelsystem.blank? and only_local.blank? next if only_engelsystem.blank? and only_local.blank?
puts "============================="
puts "Session: #{sessions[0].title} (#{engelsystem_id})" puts "============================="
puts "=============================" puts "Session: #{sessions[0].title} (#{engelsystem_id})"
puts "Not signed up in engelsystem: #{only_local.join(", ")}" unless only_local.blank? puts "============================="
puts "Missing in local assignments: #{only_engelsystem.join(", ")}" unless only_engelsystem.blank? puts "Not signed up in engelsystem: #{only_local.join(', ')}" unless only_local.blank?
puts puts "Missing in local assignments: #{only_engelsystem.join(', ')}" unless only_engelsystem.blank?
end puts
end end
return true true
end end
end end
...@@ -3,7 +3,7 @@ class FiledropFile < ApplicationRecord ...@@ -3,7 +3,7 @@ class FiledropFile < ApplicationRecord
validates :checksum, presence: true, format: { with: /\A[0-9a-fA-F]+\z/, message: "only allows hexadecimal characters" } validates :checksum, presence: true, format: { with: /\A[0-9a-fA-F]+\z/, message: "only allows hexadecimal characters" }
def sanitize_filename(filename) def sanitize_filename(filename)
filename.gsub(/[^\w\s.-]/, '_') filename.gsub(/[^\w\s.-]/, "_")
end end
def safe_download_path(download_dir, filename) def safe_download_path(download_dir, filename)
...@@ -24,7 +24,7 @@ class FiledropFile < ApplicationRecord ...@@ -24,7 +24,7 @@ class FiledropFile < ApplicationRecord
response = HTTParty.get(url) response = HTTParty.get(url)
if response.success? if response.success?
File.open(local_path, 'wb') do |file| File.open(local_path, "wb") do |file|
file.write(response.body) file.write(response.body)
end end
Rails.logger.debug("File downloaded successfully and saved as #{local_path}.") Rails.logger.debug("File downloaded successfully and saved as #{local_path}.")
...@@ -41,6 +41,6 @@ class FiledropFile < ApplicationRecord ...@@ -41,6 +41,6 @@ class FiledropFile < ApplicationRecord
session.ref_id session.ref_id
) )
FileUtils.mkdir_p(dir) FileUtils.mkdir_p(dir)
return File.join(dir, checksum) File.join(dir, checksum)
end end
end end
class Permission < ApplicationRecord
has_many :role_permissions, dependent: :destroy
has_many :roles, through: :role_permissions
validates :name, presence: true, uniqueness: { case_sensitive: false }
end
class Role < ApplicationRecord
has_many :user_roles, dependent: :destroy
has_many :users, through: :user_roles
has_many :role_permissions, dependent: :destroy
has_many :permissions, through: :role_permissions
validates :name, presence: true, uniqueness: { case_sensitive: false }
end
class RolePermission < ApplicationRecord
belongs_to :role
belongs_to :permission
end
...@@ -9,7 +9,7 @@ class Session < ApplicationRecord ...@@ -9,7 +9,7 @@ class Session < ApplicationRecord
has_many :filedrop_comments, dependent: :destroy has_many :filedrop_comments, dependent: :destroy
has_many :filedrop_files, dependent: :destroy has_many :filedrop_files, dependent: :destroy
scope :scheduled, -> { where(status: 'scheduled') } scope :scheduled, -> { where(status: "scheduled") }
scope :future, -> { where(starts_at: Time.now..) } scope :future, -> { where(starts_at: Time.now..) }
validates :ref_id, uniqueness: { scope: :conference_id } validates :ref_id, uniqueness: { scope: :conference_id }
...@@ -45,7 +45,7 @@ class Session < ApplicationRecord ...@@ -45,7 +45,7 @@ class Session < ApplicationRecord
end end
def backup_needed? def backup_needed?
return false false
end end
def assignees? def assignees?
...@@ -53,11 +53,11 @@ class Session < ApplicationRecord ...@@ -53,11 +53,11 @@ class Session < ApplicationRecord
end end
def filedrop? def filedrop?
return filedrop_files.exists? || filedrop_comments.exists? filedrop_files.exists? || filedrop_comments.exists?
end end
def duration_minutes def duration_minutes
return (ends_at - starts_at) / 60.0 (ends_at - starts_at) / 60.0
end end
private private
......
...@@ -8,7 +8,7 @@ class SessionSpeaker < ApplicationRecord ...@@ -8,7 +8,7 @@ class SessionSpeaker < ApplicationRecord
private private
def notify_on_create def notify_on_create
Rails.logger.debug('session_speaker.created') Rails.logger.debug("session_speaker.created")
ActiveSupport::Notifications.instrument( ActiveSupport::Notifications.instrument(
"session_speaker.created", "session_speaker.created",
record: self record: self
...@@ -16,7 +16,7 @@ class SessionSpeaker < ApplicationRecord ...@@ -16,7 +16,7 @@ class SessionSpeaker < ApplicationRecord
end end
def notify_on_destroy def notify_on_destroy
Rails.logger.debug('session_speaker.destroyed') Rails.logger.debug("session_speaker.destroyed")
ActiveSupport::Notifications.instrument( ActiveSupport::Notifications.instrument(
"session_speaker.destroyed", "session_speaker.destroyed",
record: self record: self
......
...@@ -4,6 +4,6 @@ class Stage < ApplicationRecord ...@@ -4,6 +4,6 @@ class Stage < ApplicationRecord
validates :ref_id, uniqueness: { scope: :conference_id } validates :ref_id, uniqueness: { scope: :conference_id }
has_many :relevant_stage_links, class_name: 'RelevantStage' has_many :relevant_stage_links, class_name: "RelevantStage"
has_many :relevant_conferences, through: :relevant_stage_links, source: :conference has_many :relevant_conferences, through: :relevant_stage_links, source: :conference
end end
...@@ -3,18 +3,23 @@ class User < ApplicationRecord ...@@ -3,18 +3,23 @@ class User < ApplicationRecord
has_many :assignments has_many :assignments
has_many :candidates has_many :candidates
has_many :user_roles, dependent: :destroy
has_many :roles, through: :user_roles
enum :darkmode, auto: 0, light: 1, dark: 2 enum :darkmode, auto: 0, light: 1, dark: 2
validates :darkmode, inclusion: { in: %w(auto light dark) } validates :darkmode, inclusion: { in: %w[auto light dark] }
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
validates :name, uniqueness: { case_sensitive: false, message: "already in use" }, allow_nil: false validates :name, uniqueness: { case_sensitive: false, message: "already in use" }, allow_nil: false
validates :email, uniqueness: { case_sensitive: false, message: "already in use" }, allow_nil: true, allow_blank: true validates :email, uniqueness: { case_sensitive: false, message: "already in use" }, allow_nil: true, allow_blank: true
before_validation :cleanup_languages before_validation :cleanup_languages
validates :languages_from, format: { with: /\A([a-z][a-z])(,[a-z][a-z])*\z/, message: "please use comma-separated two-letter codes"}, allow_blank: true validates :languages_from,
format: { with: /\A([a-z][a-z])(,[a-z][a-z])*\z/, message: "please use comma-separated two-letter codes" }, allow_blank: true
validates :languages_from, length: { maximum: 14 } validates :languages_from, length: { maximum: 14 }
validates :languages_to, format: { with: /\A([a-z][a-z])(,[a-z][a-z])*\z/, message: "please use comma-separated two-letter codes"}, allow_blank: true validates :languages_to,
format: { with: /\A([a-z][a-z])(,[a-z][a-z])*\z/, message: "please use comma-separated two-letter codes" }, allow_blank: true
validates :languages_to, length: { maximum: 14 } validates :languages_to, length: { maximum: 14 }
validates :invitation_token, presence: true, on: :create validates :invitation_token, presence: true, on: :create
...@@ -26,7 +31,7 @@ class User < ApplicationRecord ...@@ -26,7 +31,7 @@ class User < ApplicationRecord
def self.find_for_database_authentication(warden_conditions) def self.find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup conditions = warden_conditions.dup
if (login = conditions.delete(:name)) if (login = conditions.delete(:name))
where(conditions.to_h).where(["lower(name) = :value", { value: login.downcase }]).first where(conditions.to_h).where([ "lower(name) = :value", { value: login.downcase } ]).first
else else
Rails.logger.warn("Authentication did not query :name as expected, login will only work with exact case!") Rails.logger.warn("Authentication did not query :name as expected, login will only work with exact case!")
where(conditions.to_h).first where(conditions.to_h).first
...@@ -34,10 +39,10 @@ class User < ApplicationRecord ...@@ -34,10 +39,10 @@ class User < ApplicationRecord
end end
def self.leaderboard def self.leaderboard
all.map { |u| [u.name, u.workload_minutes] } all.map { |u| [ u.name, u.workload_minutes ] }
.sort_by { |_, workload| -workload } .sort_by { |_, workload| -workload }
.reject { |_, workload| workload.zero? } .reject { |_, workload| workload.zero? }
.to_h .to_h
end end
class Session < ApplicationRecord class Session < ApplicationRecord
...@@ -49,7 +54,6 @@ class User < ApplicationRecord ...@@ -49,7 +54,6 @@ class User < ApplicationRecord
end end
end end
def errors def errors
super.tap { |errors| errors.delete(:password, :blank) if password.nil? } super.tap { |errors| errors.delete(:password, :blank) if password.nil? }
end end
...@@ -59,7 +63,7 @@ class User < ApplicationRecord ...@@ -59,7 +63,7 @@ class User < ApplicationRecord
end end
def text_color def text_color
r, g, b = avatar_color.delete_prefix('#').chars.each_slice(2).map { |hex| hex.join.to_i(16) } r, g, b = avatar_color.delete_prefix("#").chars.each_slice(2).map { |hex| hex.join.to_i(16) }
# Calculate relative luminance (WCAG 2.0 formula) # Calculate relative luminance (WCAG 2.0 formula)
luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255
...@@ -69,11 +73,11 @@ class User < ApplicationRecord ...@@ -69,11 +73,11 @@ class User < ApplicationRecord
end end
def initials def initials
name.split(/\s+/).map(&:first).join('') name.split(/\s+/).map(&:first).join("")
end end
def set_avatar_color def set_avatar_color
return unless self.avatar_color.nil? return unless avatar_color.nil?
r = rand(256) r = rand(256)
g = rand(256) g = rand(256)
...@@ -87,18 +91,30 @@ class User < ApplicationRecord ...@@ -87,18 +91,30 @@ class User < ApplicationRecord
end end
def workload_minutes def workload_minutes
Assignment.includes(:session).where(user: self).sum { | a | a.session.duration_minutes } Assignment.includes(:session).where(user: self).sum { |a| a.session.duration_minutes }
end
def has_role?(role_name)
roles.exists?(name: role_name)
end
def has_permission?(permission_name)
roles.joins(:permissions).exists?(permissions: { name: permission_name })
end
def shiftcoordinator?
has_role?("shift_coordinator")
end end
private private
def valid_invitation_token def valid_invitation_token
valid_tokens = ["gargamel"] valid_tokens = [ "gargamel" ]
errors.add(:invitation_token, "is invalid") unless valid_tokens.include?(invitation_token) errors.add(:invitation_token, "is invalid") unless valid_tokens.include?(invitation_token)
end end
def cleanup_languages def cleanup_languages
self.languages_from = self.languages_from&.gsub(/\s+/, '')&.downcase self.languages_from = languages_from&.gsub(/\s+/, "")&.downcase
self.languages_to = self.languages_to&.gsub(/\s+/, '')&.downcase self.languages_to = languages_to&.gsub(/\s+/, "")&.downcase
end end
end end
class UserRole < ApplicationRecord
belongs_to :user
belongs_to :role
end
...@@ -10,6 +10,6 @@ class AssignmentAuditSubscriber ...@@ -10,6 +10,6 @@ class AssignmentAuditSubscriber
action = event.name.split(".").last action = event.name.split(".").last
record = event.payload[:record] record = event.payload[:record]
ModelVersion.create!(model: 'assignment', action:, new_data: record.to_json) ModelVersion.create!(model: "assignment", action:, new_data: record.to_json)
end end
end end
class NotificationsSubscriber class NotificationsSubscriber
def self.subscribe def self.subscribe
ActiveSupport::Notifications.subscribe('session.updated') do |*args| ActiveSupport::Notifications.subscribe("session.updated") do |*args|
event = ActiveSupport::Notifications::Event.new(*args) event = ActiveSupport::Notifications::Event.new(*args)
new.handle_event(event) new.handle_event(event)
end end
...@@ -20,7 +20,7 @@ class NotificationsSubscriber ...@@ -20,7 +20,7 @@ class NotificationsSubscriber
# ) # )
# end # end
Notification.create!( Notification.create!(
channel: 'telegram_group_chat', channel: "telegram_group_chat",
target: target:
) )
......
...@@ -12,7 +12,7 @@ class TelegramBotSubscriber ...@@ -12,7 +12,7 @@ class TelegramBotSubscriber
def handle_session_updated(event) def handle_session_updated(event)
Rails.logger.info("session event #{event.inspect}") Rails.logger.info("session event #{event.inspect}")
model_name, action = event.name.split('.') _, action = event.name.split(".")
session = event.payload[:record] session = event.payload[:record]
changes = event.payload[:changes] changes = event.payload[:changes]
...@@ -22,10 +22,10 @@ class TelegramBotSubscriber ...@@ -22,10 +22,10 @@ class TelegramBotSubscriber
%w[title language status starts_at ends_at stage_id].include? attr %w[title language status starts_at ends_at stage_id].include? attr
end end
return unless session.conference.relevant_stages.include? session.stage || changes["stage_id"].any? { |stage_id| session.conference.relevant_stages.include? Stage.find(stage_id) } return unless session.conference.relevant_stages.include?(session.stage) || changes["stage_id"].any? { |stage_id| session.conference.relevant_stages.include? Stage.find(stage_id) }
if changes["stage_id"] if changes["stage_id"]
changes["stage"] = [Stage.find(changes["stage_id"].first).name, Stage.find(changes["stage_id"].last).name] changes["stage"] = [ Stage.find(changes["stage_id"].first).name, Stage.find(changes["stage_id"].last).name ]
changes.delete("stage_id") changes.delete("stage_id")
end end
...@@ -37,7 +37,7 @@ class TelegramBotSubscriber ...@@ -37,7 +37,7 @@ class TelegramBotSubscriber
def handle_session_speaker_event(event) def handle_session_speaker_event(event)
Rails.logger.info("session_speaker event #{event.inspect}") Rails.logger.info("session_speaker event #{event.inspect}")
model_name, action = event.name.split('.') model_name, action = event.name.split(".")
session_speaker = event.payload[:record] session_speaker = event.payload[:record]
session = session_speaker.session session = session_speaker.session
......
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-6 dark:text-gray-200">Admin: Edit Conference: <%= @conference.name %></h1>
<%= form_with(model: [:admin, @conference], url: admin_conference_path(slug: @conference.slug), method: :patch, class: "space-y-6", data: { controller: "conference-form" }) do |form| %>
<% if @conference.errors.any? %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<h2 class="font-bold"><%= pluralize(@conference.errors.count, "error") %> prohibited this conference from being saved:</h2>
<ul class="list-disc list-inside mt-2">
<% @conference.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="space-y-4">
<div>
<%= form.label :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :name, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :slug, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :slug, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Used in URLs. Should contain only lowercase letters, numbers, and hyphens.</p>
</div>
<div>
<%= form.label :url, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.url_field :url, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :time_zone, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.time_zone_select :time_zone, nil, {}, { class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" } %>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<%= form.label :starts_at, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.datetime_local_field :starts_at, value: @conference.starts_at&.strftime('%Y-%m-%dT%H:%M'), class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :ends_at, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.datetime_local_field :ends_at, value: @conference.ends_at&.strftime('%Y-%m-%dT%H:%M'), class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
</div>
<div>
<%= form.label :import_job_class, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.select :import_job_class,
[["Select Import Job Class", ""]] +
ConferencesController.available_import_job_classes.map { |class_name, display_name| [display_name, class_name] },
{},
{ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white",
data: { conference_form_target: "importJobClass", action: "change->conference-form#importJobClassChanged" } } %>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Select the import job class to see required data fields</p>
</div>
<!-- Add hidden inputs to preserve all data values -->
<% if @conference.data.present? %>
<% @conference.data.each do |key, value| %>
<input type="hidden" name="data[<%= key %>]" value="<%= value %>" id="hidden_data_<%= key %>">
<% end %>
<% end %>
<fieldset class="mt-6 border border-gray-300 rounded-md p-4 dark:border-gray-600">
<legend class="px-2 text-sm font-medium text-gray-700 dark:text-gray-300">Required Data Fields</legend>
<div class="space-y-4" data-conference-form-target="requiredFields">
<% if @conference.import_job_class.present? %>
<% begin %>
<% klass = @conference.import_job_class.constantize %>
<% @conference.required_data_fields.each do |field| %>
<% metadata = klass.respond_to?(:field_metadata) ? (klass.field_metadata[field] || {}) : {} %>
<div class="mb-4">
<label for="data_<%= field %>" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
<%= metadata[:title] || field.humanize %><%= metadata[:required] ? ' <span class="text-red-500">*</span>'.html_safe : '' %>
</label>
<% if metadata[:description].present? %>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400"><%= metadata[:description] %></p>
<% end %>
<input
type="text"
id="data_<%= field %>"
name="data[<%= field %>]"
value="<%= @conference.data&.dig(field) %>"
placeholder="<%= metadata[:placeholder] %>"
<%= 'required' if metadata[:required] %>
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<% end %>
<% rescue => e %>
<div class="text-red-500">Error loading field metadata: <%= e.message %></div>
<% end %>
<% else %>
<p class="text-sm text-gray-500 dark:text-gray-400">Select an import job class to see required fields</p>
<% end %>
</div>
</fieldset>
<fieldset class="mt-6 border border-gray-300 rounded-md p-4 dark:border-gray-600">
<legend class="px-2 text-sm font-medium text-gray-700 dark:text-gray-300">Custom Data Fields</legend>
<div class="space-y-4" data-conference-form-target="customFields">
<% if @conference.data.present? %>
<% @conference.data.except(*@conference.required_data_fields).each do |key, value| %>
<div class="flex items-center space-x-2 custom-field-row">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Key</label>
<input type="text" name="custom_field_keys[]" value="<%= key %>" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Value</label>
<input type="text" name="custom_field_values[]" value="<%= value %>" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="flex items-end">
<button type="button" data-action="click->conference-form#removeCustomField" class="mt-1 p-2 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
<% end %>
<% end %>
</div>
<template data-conference-form-target="customTemplate">
<div class="flex items-center space-x-2 custom-field-row">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Key</label>
<input type="text" name="custom_field_keys[]" value="KEY_PLACEHOLDER" placeholder="Enter key" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Value</label>
<input type="text" name="custom_field_values[]" value="VALUE_PLACEHOLDER" placeholder="Enter value" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="flex items-end">
<button type="button" data-action="click->conference-form#removeCustomField" class="mt-1 p-2 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</template>
<div class="mt-4">
<button type="button" data-action="click->conference-form#addCustomField" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-indigo-900 dark:text-indigo-200 dark:hover:bg-indigo-800">
<svg xmlns="http://www.w3.org/2000/svg" class="-ml-0.5 mr-2 h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
Add Custom Field
</button>
</div>
</fieldset>
</div>
<div class="flex justify-between pt-6">
<%= link_to "Cancel", admin_conferences_path, class: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600" %>
<%= form.submit "Update Conference", class: "inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-blue-700 dark:hover:bg-blue-800" %>
</div>
<% end %>
<div class="mt-8 pt-8 border-t border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold mb-4 dark:text-gray-200">Danger Zone</h2>
<div class="bg-red-50 border border-red-300 rounded-md p-4 dark:bg-red-900/20 dark:border-red-800">
<h3 class="text-lg font-medium text-red-800 dark:text-red-300">Delete This Conference</h3>
<p class="mt-1 text-sm text-red-700 dark:text-red-400">Once you delete a conference, there is no going back. This will delete all associated data including sessions, speakers, and stages.</p>
<div class="mt-4">
<%= button_to "Delete Conference", admin_conference_path(slug: @conference.slug), method: :delete,
data: { confirm: "Are you sure you want to delete this conference? This action cannot be undone." },
class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 dark:bg-red-700 dark:hover:bg-red-800" %>
</div>
</div>
</div>
</div>
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold dark:text-gray-200">Admin: Conferences</h1>
<%= link_to "New Conference", new_admin_conference_path, class: "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-800" %>
</div>
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg">
<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">Name</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">Slug</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">Dates</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">Import Job</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">Actions</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
<% @conferences.each do |conference| %>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-200">
<%= link_to conference.name, conference_path(slug: conference.slug), class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" %>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= conference.slug %></td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
<%= conference.starts_at&.strftime('%Y-%m-%d') %> to <%= conference.ends_at&.strftime('%Y-%m-%d') %>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= conference.import_job_class %></td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div class="flex space-x-2">
<%= link_to "Edit", edit_admin_conference_path(slug: conference.slug), class: "text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300" %>
<%= button_to "Delete", admin_conference_path(slug: conference.slug), method: :delete,
data: { confirm: "Are you sure you want to delete this conference? This action cannot be undone." },
class: "text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 bg-transparent border-none cursor-pointer" %>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-6 dark:text-gray-200">Admin: New Conference</h1>
<%= form_with(model: [:admin, @conference], class: "space-y-6", data: { controller: "conference-form" }) do |form| %>
<% if @conference.errors.any? %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<h2 class="font-bold"><%= pluralize(@conference.errors.count, "error") %> prohibited this conference from being saved:</h2>
<ul class="list-disc list-inside mt-2">
<% @conference.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="space-y-4">
<div>
<%= form.label :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :name, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :slug, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :slug, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Used in URLs. Should contain only lowercase letters, numbers, and hyphens.</p>
</div>
<div>
<%= form.label :url, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.url_field :url, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :time_zone, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.time_zone_select :time_zone, nil, { include_blank: "Select Time Zone" }, { class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" } %>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<%= form.label :starts_at, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.datetime_local_field :starts_at, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :ends_at, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.datetime_local_field :ends_at, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
</div>
<div>
<%= form.label :import_job_class, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.select :import_job_class,
[["Select Import Job Class", ""]] +
ConferencesController.available_import_job_classes.map { |class_name, display_name| [display_name, class_name] },
{},
{ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white",
data: { conference_form_target: "importJobClass", action: "change->conference-form#importJobClassChanged" } } %>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Select the import job class to see required data fields</p>
</div>
<fieldset class="mt-6 border border-gray-300 rounded-md p-4 dark:border-gray-600">
<legend class="px-2 text-sm font-medium text-gray-700 dark:text-gray-300">Required Data Fields</legend>
<div class="space-y-4" data-conference-form-target="requiredFields">
<p class="text-sm text-gray-500 dark:text-gray-400">Select an import job class to see required fields</p>
</div>
</fieldset>
<fieldset class="mt-6 border border-gray-300 rounded-md p-4 dark:border-gray-600">
<legend class="px-2 text-sm font-medium text-gray-700 dark:text-gray-300">Custom Data Fields</legend>
<div class="space-y-4" data-conference-form-target="customFields">
<!-- Custom fields will be added here -->
</div>
<template data-conference-form-target="customTemplate">
<div class="flex items-center space-x-2 custom-field-row">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Key</label>
<input type="text" name="custom_field_keys[]" value="KEY_PLACEHOLDER" placeholder="Enter key" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Value</label>
<input type="text" name="custom_field_values[]" value="VALUE_PLACEHOLDER" placeholder="Enter value" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="flex items-end">
<button type="button" data-action="click->conference-form#removeCustomField" class="mt-1 p-2 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</template>
<div class="mt-4">
<button type="button" data-action="click->conference-form#addCustomField" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-indigo-900 dark:text-indigo-200 dark:hover:bg-indigo-800">
<svg xmlns="http://www.w3.org/2000/svg" class="-ml-0.5 mr-2 h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
Add Custom Field
</button>
</div>
</fieldset>
</div>
<div class="flex justify-between pt-6">
<%= link_to "Cancel", admin_conferences_path, class: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600" %>
<%= form.submit "Create Conference", class: "inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-blue-700 dark:hover:bg-blue-800" %>
</div>
<% end %>
</div>
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-6 dark:text-gray-200">Admin: Edit Role: <%= @role.name %></h1>
<%= form_with(model: [:admin, @role], class: "space-y-6") do |form| %>
<% if @role.errors.any? %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<h2 class="font-bold"><%= pluralize(@role.errors.count, "error") %> prohibited this role from being saved:</h2>
<ul class="list-disc list-inside mt-2">
<% @role.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="space-y-4">
<div>
<%= form.label :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :name, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :description, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_area :description, rows: 3, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<fieldset class="mt-6 border border-gray-300 rounded-md p-4 dark:border-gray-600">
<legend class="px-2 text-sm font-medium text-gray-700 dark:text-gray-300">Permissions</legend>
<div class="space-y-2 mt-2">
<% @permissions.each do |permission| %>
<div class="flex items-center">
<%= check_box_tag "role[permission_ids][]", permission.id, @role.permissions.include?(permission), id: "permission_#{permission.id}", class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600" %>
<%= label_tag "permission_#{permission.id}", permission.name, class: "ml-2 block text-sm text-gray-900 dark:text-gray-300" %>
<% if permission.description.present? %>
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400"><%= permission.description %></span>
<% end %>
</div>
<% end %>
</div>
</fieldset>
</div>
<div class="flex justify-between pt-6">
<%= link_to "Cancel", admin_roles_path, class: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600" %>
<%= form.submit "Update Role", class: "inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-blue-700 dark:hover:bg-blue-800" %>
</div>
<% end %>
</div>
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-6 dark:text-gray-200">Admin: Roles and Permissions</h1>
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg">
<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">Role</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">Description</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">Permissions</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">Actions</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
<% @roles.each do |role| %>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-200">
<%= role.name %>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
<%= role.description %>
</td>
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
<div class="flex flex-wrap gap-1">
<% role.permissions.each do |permission| %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
<%= permission.name %>
</span>
<% end %>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<%= link_to "Edit", edit_admin_role_path(role), class: "text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300" %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<div>
<h1 class="font-bold text-4xl">Admin::Roles#update</h1>
<p>Find me in app/views/admin/roles/update.html.erb</p>
</div>
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-6 dark:text-gray-200">Edit Conference: <%= @conference.name %></h1>
<%= form_with(model: @conference, url: conference_path(slug: @conference.slug), method: :patch, class: "space-y-6") do |form| %>
<% if @conference.errors.any? %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<h2 class="font-bold"><%= pluralize(@conference.errors.count, "error") %> prohibited this conference from being saved:</h2>
<ul class="list-disc list-inside mt-2">
<% @conference.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="space-y-4">
<div>
<%= form.label :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :name, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :slug, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :slug, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Used in URLs. Should contain only lowercase letters, numbers, and hyphens.</p>
</div>
<div>
<%= form.label :url, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.url_field :url, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :time_zone, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.time_zone_select :time_zone, nil, {}, { class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" } %>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<%= form.label :starts_at, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.datetime_local_field :starts_at, value: @conference.starts_at&.strftime('%Y-%m-%dT%H:%M'), class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :ends_at, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.datetime_local_field :ends_at, value: @conference.ends_at&.strftime('%Y-%m-%dT%H:%M'), class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
</div>
<div data-controller="dynamic-fields">
<div>
<%= form.label :import_job_class, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.select :import_job_class,
[["Select Import Job Class", ""]] +
ConferencesController.available_import_job_classes.map { |class_name, display_name| [display_name, class_name] },
{},
{ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white",
data: { dynamic_fields_target: "importJobClass", action: "change->dynamic-fields#updateRequiredFields" } } %>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Select the import job class to see required data fields</p>
</div>
<fieldset class="mt-6 border border-gray-300 rounded-md p-4 dark:border-gray-600">
<legend class="px-2 text-sm font-medium text-gray-700 dark:text-gray-300">Required Data Fields</legend>
<div class="space-y-4" data-dynamic-fields-target="requiredFields">
<% @conference.required_data_fields.each do |field| %>
<div class="mb-4">
<%= form.label "data[#{field}]", field.humanize, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.url_field "data[#{field}]", value: @conference.data&.dig(field), class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<% end %>
<% if @conference.required_data_fields.empty? %>
<p class="text-sm text-gray-500 dark:text-gray-400">No required fields for this import job class</p>
<% end %>
</div>
</fieldset>
<fieldset class="mt-6 border border-gray-300 rounded-md p-4 dark:border-gray-600" data-controller="dynamic-fields">
<legend class="px-2 text-sm font-medium text-gray-700 dark:text-gray-300">Custom Data Fields</legend>
<div class="space-y-4" data-dynamic-fields-target="container">
<% if @conference.data.present? %>
<% @conference.data.except('schedule_url', 'filedrop_url', 'engelsystem_url', 'heartbeat_url').each do |key, value| %>
<div class="flex items-center space-x-2 nested-field">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Key</label>
<input type="text" value="<%= key %>" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" data-field-key>
</div>
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Value</label>
<input type="text" name="data[<%= key %>]" value="<%= value %>" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="flex items-end">
<button type="button" data-action="click->dynamic-fields#removeField" class="mt-1 p-2 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
<% end %>
<% end %>
</div>
<template data-dynamic-fields-target="template">
<div class="flex items-center space-x-2 nested-field">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Key</label>
<input type="text" name="data[custom_key_NEW_RECORD]" placeholder="Enter key" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" data-field-key>
</div>
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Value</label>
<input type="text" name="data[custom_value_NEW_RECORD]" placeholder="Enter value" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="flex items-end">
<button type="button" data-action="click->dynamic-fields#removeField" class="mt-1 p-2 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</template>
<div class="mt-4">
<button type="button" data-action="click->dynamic-fields#addField" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-indigo-900 dark:text-indigo-200 dark:hover:bg-indigo-800">
<svg xmlns="http://www.w3.org/2000/svg" class="-ml-0.5 mr-2 h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
Add Custom Field
</button>
</div>
</fieldset>
</div>
<div class="flex justify-between pt-6">
<%= link_to "Cancel", conference_path(slug: @conference.slug), class: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600" %>
<%= form.submit "Update Conference", class: "inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-blue-700 dark:hover:bg-blue-800" %>
</div>
<% end %>
<div class="mt-8 pt-8 border-t border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold mb-4 dark:text-gray-200">Danger Zone</h2>
<div class="bg-red-50 border border-red-300 rounded-md p-4 dark:bg-red-900/20 dark:border-red-800">
<h3 class="text-lg font-medium text-red-800 dark:text-red-300">Delete This Conference</h3>
<p class="mt-1 text-sm text-red-700 dark:text-red-400">Once you delete a conference, there is no going back. This will delete all associated data including sessions, speakers, and stages.</p>
<div class="mt-4">
<%= button_to "Delete Conference", conference_path(slug: @conference.slug), method: :delete,
data: { confirm: "Are you sure you want to delete this conference? This action cannot be undone." },
class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 dark:bg-red-700 dark:hover:bg-red-800" %>
</div>
</div>
</div>
</div>