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

Target

Select target project
  • c3lingo/rescheduled
1 result
Show changes
Showing
with 718 additions and 78 deletions
......@@ -8,7 +8,7 @@ class Conference < ApplicationRecord
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
def days
......@@ -20,52 +20,64 @@ class Conference < ApplicationRecord
end
def starts_at_in_local_time
starts_at.in_time_zone(time_zone || 'UTC')
starts_at.in_time_zone(time_zone || "UTC")
end
def ends_at_in_local_time
ends_at.in_time_zone(time_zone || 'UTC')
ends_at.in_time_zone(time_zone || "UTC")
end
def schedule_url
data['schedule_url']
data["schedule_url"]
end
def filedrop_url
data['filedrop_url']
data["filedrop_url"]
end
def engelsystem_url
data['engelsystem_url']
data["engelsystem_url"]
end
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
def fetch_translation_angel_id
fetch_engelsystem("angeltypes")
&.find { |t| t['name'] == 'Translation Angel' }
&.dig('id')
&.find { |t| t["name"] == "Translation Angel" }
&.dig("id")
end
def fetch_engelsystem(endpoint)
begin
endpoint_url = engelsystem_url + endpoint
Rails.logger.debug("Querying engelsystem API at #{endpoint_url}")
response = HTTParty.get(
endpoint_url,
headers: {
'Accept' => 'application/json',
"x-api-key" => fetch_credential("engelsystem_token")
},
timeout: 10
)
return response.success? ? JSON.parse(response.body)["data"] : nil
rescue => e
Rails.logger.warn("Engelsystem query for #{endpoint} failed: #{e.message}")
return nil
end
endpoint_url = engelsystem_url + endpoint
Rails.logger.debug("Querying engelsystem API at #{endpoint_url}")
response = HTTParty.get(
endpoint_url,
headers: {
"Accept" => "application/json",
"x-api-key" => fetch_credential("engelsystem_token")
},
timeout: 10
)
response.success? ? JSON.parse(response.body)["data"] : nil
rescue StandardError => e
Rails.logger.warn("Engelsystem query for #{endpoint} failed: #{e.message}")
nil
end
def compare_engelsystem_shifts(additional_conferences = [])
......@@ -73,35 +85,35 @@ class Conference < ApplicationRecord
return unless data = fetch_engelsystem("angeltypes/#{translation_angel_id}/shifts")
engelsystem_shifts = data.each_with_object({}) do |shift, hash|
hash[shift['id']] = shift
&.dig("needed_angel_types")
&.find{ |t| t["angel_type"]["id"] == translation_angel_id }
&.dig("entries")
&.map{ |t| t["user"]["name"] }
hash[shift["id"]] = shift
&.dig("needed_angel_types")
&.find { |t| t["angel_type"]["id"] == translation_angel_id }
&.dig("entries")
&.map { |t| t["user"]["name"] }
end
Session
.where(conference: [self, *additional_conferences])
.where.not(engelsystem_id: nil)
.includes(assignments: :user)
.group_by(&:engelsystem_id)
.each do |engelsystem_id, sessions|
.where(conference: [ self, *additional_conferences ])
.where.not(engelsystem_id: nil)
.includes(assignments: :user)
.group_by(&:engelsystem_id)
.each do |engelsystem_id, sessions|
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_local = local_assigned - engelsystem_assigned
unless only_engelsystem.blank? and only_local.blank?
puts "============================="
puts "Session: #{sessions[0].title} (#{engelsystem_id})"
puts "============================="
puts "Not signed up in engelsystem: #{only_local.join(", ")}" unless only_local.blank?
puts "Missing in local assignments: #{only_engelsystem.join(", ")}" unless only_engelsystem.blank?
puts
end
next if only_engelsystem.blank? and only_local.blank?
puts "============================="
puts "Session: #{sessions[0].title} (#{engelsystem_id})"
puts "============================="
puts "Not signed up in engelsystem: #{only_local.join(', ')}" unless only_local.blank?
puts "Missing in local assignments: #{only_engelsystem.join(', ')}" unless only_engelsystem.blank?
puts
end
return true
true
end
end
......@@ -3,7 +3,7 @@ class FiledropFile < ApplicationRecord
validates :checksum, presence: true, format: { with: /\A[0-9a-fA-F]+\z/, message: "only allows hexadecimal characters" }
def sanitize_filename(filename)
filename.gsub(/[^\w\s.-]/, '_')
filename.gsub(/[^\w\s.-]/, "_")
end
def safe_download_path(download_dir, filename)
......@@ -24,7 +24,7 @@ class FiledropFile < ApplicationRecord
response = HTTParty.get(url)
if response.success?
File.open(local_path, 'wb') do |file|
File.open(local_path, "wb") do |file|
file.write(response.body)
end
Rails.logger.debug("File downloaded successfully and saved as #{local_path}.")
......@@ -41,6 +41,6 @@ class FiledropFile < ApplicationRecord
session.ref_id
)
FileUtils.mkdir_p(dir)
return File.join(dir, checksum)
File.join(dir, checksum)
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
has_many :filedrop_comments, 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..) }
validates :ref_id, uniqueness: { scope: :conference_id }
......@@ -45,7 +45,7 @@ class Session < ApplicationRecord
end
def backup_needed?
return false
false
end
def assignees?
......@@ -53,11 +53,11 @@ class Session < ApplicationRecord
end
def filedrop?
return filedrop_files.exists? || filedrop_comments.exists?
filedrop_files.exists? || filedrop_comments.exists?
end
def duration_minutes
return (ends_at - starts_at) / 60.0
(ends_at - starts_at) / 60.0
end
private
......
......@@ -8,7 +8,7 @@ class SessionSpeaker < ApplicationRecord
private
def notify_on_create
Rails.logger.debug('session_speaker.created')
Rails.logger.debug("session_speaker.created")
ActiveSupport::Notifications.instrument(
"session_speaker.created",
record: self
......@@ -16,7 +16,7 @@ class SessionSpeaker < ApplicationRecord
end
def notify_on_destroy
Rails.logger.debug('session_speaker.destroyed')
Rails.logger.debug("session_speaker.destroyed")
ActiveSupport::Notifications.instrument(
"session_speaker.destroyed",
record: self
......
......@@ -4,6 +4,6 @@ class Stage < ApplicationRecord
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
end
......@@ -3,18 +3,23 @@ class User < ApplicationRecord
has_many :assignments
has_many :candidates
has_many :user_roles, dependent: :destroy
has_many :roles, through: :user_roles
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 :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
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_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 :invitation_token, presence: true, on: :create
......@@ -26,7 +31,7 @@ class User < ApplicationRecord
def self.find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup
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
Rails.logger.warn("Authentication did not query :name as expected, login will only work with exact case!")
where(conditions.to_h).first
......@@ -34,10 +39,10 @@ class User < ApplicationRecord
end
def self.leaderboard
all.map { |u| [u.name, u.workload_minutes] }
.sort_by { |_, workload| -workload }
.reject { |_, workload| workload.zero? }
.to_h
all.map { |u| [ u.name, u.workload_minutes ] }
.sort_by { |_, workload| -workload }
.reject { |_, workload| workload.zero? }
.to_h
end
class Session < ApplicationRecord
......@@ -49,7 +54,6 @@ class User < ApplicationRecord
end
end
def errors
super.tap { |errors| errors.delete(:password, :blank) if password.nil? }
end
......@@ -59,7 +63,7 @@ class User < ApplicationRecord
end
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)
luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255
......@@ -69,11 +73,11 @@ class User < ApplicationRecord
end
def initials
name.split(/\s+/).map(&:first).join('')
name.split(/\s+/).map(&:first).join("")
end
def set_avatar_color
return unless self.avatar_color.nil?
return unless avatar_color.nil?
r = rand(256)
g = rand(256)
......@@ -87,18 +91,30 @@ class User < ApplicationRecord
end
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
private
def valid_invitation_token
valid_tokens = ["gargamel"]
valid_tokens = [ "gargamel" ]
errors.add(:invitation_token, "is invalid") unless valid_tokens.include?(invitation_token)
end
def cleanup_languages
self.languages_from = self.languages_from&.gsub(/\s+/, '')&.downcase
self.languages_to = self.languages_to&.gsub(/\s+/, '')&.downcase
self.languages_from = languages_from&.gsub(/\s+/, "")&.downcase
self.languages_to = languages_to&.gsub(/\s+/, "")&.downcase
end
end
class UserRole < ApplicationRecord
belongs_to :user
belongs_to :role
end
......@@ -10,6 +10,6 @@ class AssignmentAuditSubscriber
action = event.name.split(".").last
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
class NotificationsSubscriber
def self.subscribe
ActiveSupport::Notifications.subscribe('session.updated') do |*args|
ActiveSupport::Notifications.subscribe("session.updated") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
new.handle_event(event)
end
......@@ -20,7 +20,7 @@ class NotificationsSubscriber
# )
# end
Notification.create!(
channel: 'telegram_group_chat',
channel: "telegram_group_chat",
target:
)
......
......@@ -12,7 +12,7 @@ class TelegramBotSubscriber
def handle_session_updated(event)
Rails.logger.info("session event #{event.inspect}")
model_name, action = event.name.split('.')
_, action = event.name.split(".")
session = event.payload[:record]
changes = event.payload[:changes]
......@@ -22,10 +22,10 @@ class TelegramBotSubscriber
%w[title language status starts_at ends_at stage_id].include? attr
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"]
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")
end
......@@ -37,7 +37,7 @@ class TelegramBotSubscriber
def handle_session_speaker_event(event)
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 = 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>