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
Commits on Source (4)
  • Teal Bauer's avatar
    Admin improvements, RBAC and styling fixes · 5f4e3c6c
    Teal Bauer authored
    Extend admin interface:
    
    - Make admin controllers inherit from Admin::BaseController
    - New admin dashboard with system statistics overview
    - Centralized admin layout with dedicated navigation
    - Enhanced user management interface with role assignment capabilities
    
    RBAC fixes:
    
    - Introduced new `admin` role that has all rights
    - Added db migration to create admin role with appropriate permissions
    - Updated admin rake tasks for role management
    - Updated db seeds to add all required roles properly
    
    Styling improvements and fixes:
    
    - Upgraded button system with standardized styling and dark mode support
    - Improved UI with consistent styling using Tailwind components
    - Brought back manual dark mode using Tailwind custom variant
    5f4e3c6c
  • Teal's avatar
    Merge branch 'admin-improvements-and-styling-fixes' into 'main' · 375f17e8
    Teal authored
    Admin improvements, RBAC and styling fixes
    
    See merge request !26
    375f17e8
  • Teal Bauer's avatar
    Improve UI for users · 379e83ee
    Teal Bauer authored and Teal's avatar Teal committed
    - Improve "My Assignments" style with the new table styles
    - Also, bring back Conferences / Assignments
    - Change menu layout back to old nav
    - Dark mode fixes
    379e83ee
  • Teal's avatar
    Merge branch 'user-styling-improvements' into 'main' · 7447aa53
    Teal authored
    Improve UI for users
    
    See merge request !27
    7447aa53
Showing
with 865 additions and 376 deletions
......@@ -2,20 +2,59 @@
@tailwind components;
@tailwind utilities;
/*
@custom-variant dark (&:where(.dark, .dark *));
@layer components {
/* Button Base Styles */
.btn {
@apply py-2 px-4 font-medium rounded inline-block text-center no-underline border border-transparent cursor-pointer;
}
/* Primary Button */
.btn-primary {
@apply py-2 px-4 bg-blue-200;
@apply bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-800;
}
/* Secondary Button */
.btn-secondary {
@apply bg-gray-200 text-gray-800 border-gray-300 hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:border-gray-700 dark:hover:bg-gray-700;
}
/* Tertiary Button */
.btn-tertiary {
@apply bg-gray-600 text-white hover:bg-gray-700 dark:bg-gray-700 dark:hover:bg-gray-800;
}
/* Danger Button */
.btn-danger {
@apply bg-red-600 text-white hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800;
}
/* Success Button */
.btn-success {
@apply bg-green-600 text-white hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-800;
}
/* Info Button */
.btn-info {
@apply bg-indigo-600 text-white hover:bg-indigo-700 dark:bg-indigo-700 dark:hover:bg-indigo-800;
}
/* Light Info Button */
.btn-info-light {
@apply bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900 dark:text-indigo-200 dark:hover:bg-indigo-800;
}
}
*/
/* Light Success Button */
.btn-success-light {
@apply bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900 dark:text-green-200 dark:hover:bg-green-800;
}
}
h1, h2, h3, h4, h5, h6 {
@apply font-bold;
}
input[type=submit] {
{{/* input[type=submit] {
@apply border bg-gray-400 rounded-md px-2 py-1;
&.primary {
@apply bg-teal-800 text-teal-50 border-teal-600;
......@@ -37,7 +76,7 @@ select, [type=text], [type=password] {
@apply bg-white/60;
}
}
}
} */}}
.session-holder {
@apply w-full;
......@@ -79,11 +118,6 @@ select, [type=text], [type=password] {
}
}
}
.main-nav {
a {
@apply underline hover:text-blue-600 hover:border-blue-600;
}
}
.past {
@apply opacity-50 hover:opacity-100;
}
......
class Admin::BaseController < ApplicationController
before_action :authenticate_user!
layout "admin"
protected
def authorize_admin
unless current_user&.has_permission?("manage_users") || current_user&.has_permission?("manage_conferences")
redirect_to root_path, alert: "You are not authorized to access this section."
end
end
end
module Admin
class ConferencesController < ApplicationController
before_action :authenticate_user!
class ConferencesController < Admin::BaseController
before_action :authorize_permission
before_action :set_conference, except: [ :index, :new, :create ]
......
module Admin
class DashboardController < Admin::BaseController
def index
@conferences_count = Conference.count
@users_count = User.count
@sessions_count = Session.count
@assignments_count = Assignment.count
@recent_imports = ImportHistory.order(created_at: :desc).limit(5)
@admin_users = Role.find_by(name: "admin")&.users || []
@shift_coordinators = Role.find_by(name: "shift_coordinator")&.users || []
@events_admins = Role.find_by(name: "events_admin")&.users || []
end
end
end
module Admin
class RolesController < ApplicationController
before_action :authenticate_user!
class RolesController < Admin::BaseController
before_action :authorize_role
before_action :set_role, only: [ :edit, :update ]
......
class Admin::UsersController < Admin::BaseController
before_action :authorize_admin
before_action :set_user, only: [ :show, :edit, :update, :destroy ]
def index
@users = User.all.order(:name)
end
def show
end
def new
@user = User.new
end
def edit
@available_roles = Role.all
end
def create
@user = User.new(user_params)
if @user.save
update_user_roles
redirect_to admin_users_path, notice: "User was successfully created."
else
render :new, status: :unprocessable_entity
end
end
def update
if @user.update(user_params)
update_user_roles
redirect_to admin_users_path, notice: "User was successfully updated."
else
@available_roles = Role.all
render :edit, status: :unprocessable_entity
end
end
def destroy
@user.destroy
redirect_to admin_users_path, notice: "User was successfully deleted."
end
private
def set_user
@user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation, :telegram_username, :languages_from, :languages_to)
end
def update_user_roles
# Clear existing roles
@user.roles.clear
# Add selected roles
if params[:user][:role_ids].present?
params[:user][:role_ids].each do |role_id|
next if role_id.blank?
role = Role.find(role_id)
@user.roles << role
end
end
end
def authorize_admin
unless current_user&.has_permission?("manage_users")
redirect_to root_path, alert: "You are not authorized to access this section."
end
end
end
module ButtonHelper
def button_primary_class
"px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-800"
end
def button_secondary_class
"px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
end
def button_tertiary_class
"px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700 dark:bg-gray-700 dark:hover:bg-gray-800"
end
def button_danger_class
"px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800"
end
def button_success_class
"px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-800"
end
def button_info_class
"px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700 dark:bg-indigo-700 dark:hover:bg-indigo-800"
end
def button_light_info_class
"px-4 py-2 bg-indigo-100 text-indigo-700 rounded hover:bg-indigo-200 dark:bg-indigo-900 dark:text-indigo-200 dark:hover:bg-indigo-800"
end
def button_light_success_class
"px-4 py-2 bg-green-100 text-green-700 rounded hover:bg-green-200 dark:bg-green-900 dark:text-green-200 dark:hover:bg-green-800"
end
end
class ImportHistory < ApplicationRecord
belongs_to :conference
enum :status, started: 0, completed: 1, failed: 2
enum :status, {
started: "started",
completed: "completed",
failed: "failed"
}
def duration
return nil unless started_at
......
<%= form_with(model: [:admin, conference], url: url, method: method, class: "space-y-6", data: { controller: "conference-form" }) do |form| %>
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<% 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, 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>
<div class="flex justify-between pt-6">
<div class="space-x-2">
<%= link_to "Cancel", admin_conferences_path, class: "btn btn-secondary" %>
<% if conference.persisted? %>
<%= link_to "View Import History", import_history_admin_conference_path(conference), class: "btn btn-info-light" %>
<% end %>
</div>
<div class="space-x-2">
<% if conference.persisted? %>
<%= link_to "Manage Relevant Stages", select_relevant_stages_admin_conference_path(conference), class: "btn btn-success-light" %>
<% end %>
<%= form.submit submit_text, class: "btn btn-primary" %>
</div>
</div>
<% end %>
<% if conference.persisted? %>
<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: "btn btn-danger" %>
</div>
</div>
</div>
<% end %>
<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">
<div class="space-x-2">
<%= 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" %>
<% if @conference.persisted? %>
<%= link_to "View Import History", import_history_admin_conference_path(@conference), class: "px-4 py-2 text-sm font-medium text-indigo-700 bg-indigo-100 border border-transparent rounded-md shadow-sm hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-indigo-900 dark:text-indigo-200 dark:hover:bg-indigo-800" %>
<% end %>
</div>
<div class="space-x-2">
<% if @conference.persisted? %>
<%= link_to "Manage Relevant Stages", select_relevant_stages_admin_conference_path(@conference), class: "px-4 py-2 text-sm font-medium text-green-700 bg-green-100 border border-transparent rounded-md shadow-sm hover:bg-green-200 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 dark:bg-green-900 dark:text-green-200 dark:hover:bg-green-800" %>
<% end %>
<%= 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>
</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>
<%= render "form", conference: @conference, url: admin_conference_path(slug: @conference.slug), method: :patch, submit_text: "Update Conference" %>
</div>
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-6">Import History for <%= @conference.name %></h1>
<div class="mb-6">
<%= link_to "Back to Conference", edit_admin_conference_path(@conference), class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" %>
<span class="mx-2">|</span>
<%= button_to "Run Import Now", retry_import_admin_conference_path(@conference), method: :post, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-blue-700 dark:hover:bg-blue-800" %>
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold dark:text-gray-200">Import History for <%= @conference.name %></h1>
<div class="flex space-x-2">
<%= button_to "Run Import Now", retry_import_admin_conference_path(@conference), method: :post, class: "btn btn-primary" %>
<%= link_to "Back to Conference", admin_conference_path(@conference), class: "btn btn-tertiary" %>
</div>
</div>
<div class="bg-white shadow overflow-hidden sm:rounded-lg dark:bg-gray-800">
......@@ -35,11 +35,11 @@
<%= history.created_at.strftime("%Y-%m-%d %H:%M:%S") %>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<% if history.status == 'completed' %>
<% if history.completed? %>
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
Completed
</span>
<% elsif history.status == 'failed' %>
<% elsif history.failed? %>
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
Failed
</span>
......
<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" %>
<%= link_to "New Conference", new_admin_conference_path, class: "btn btn-primary" %>
</div>
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg">
......@@ -28,7 +28,9 @@
<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 "View", admin_conference_path(slug: conference.slug), class: "text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300" %>
<%= 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" %>
<%= link_to "Import History", import_history_admin_conference_path(slug: conference.slug), class: "text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-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 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 %>
<%= render "form", conference: @conference, url: admin_conferences_path, method: :post, submit_text: "Create Conference" %>
</div>
......@@ -21,8 +21,8 @@
<% end %>
<div class="mt-4 flex space-x-4">
<%= button_to "Retry Import", retry_import_admin_conference_path(@conference), method: :post, class: "px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700" %>
<%= link_to "View Error Details", import_error_admin_conference_path(@conference), class: "px-4 py-2 border border-red-600 text-red-600 rounded hover:bg-red-50", data: { action: "view-error" } %>
<%= button_to "Retry Import", retry_import_admin_conference_path(@conference), method: :post, class: "btn btn-danger" %>
<%= link_to "View Error Details", import_error_admin_conference_path(@conference), class: "btn btn-secondary", data: { action: "view-error" } %>
</div>
</div>
......@@ -55,7 +55,7 @@
<% if @conference.last_import_completed_at %>
<p class="mt-2">Last import completed at <%= @conference.last_import_completed_at.strftime("%Y-%m-%d %H:%M:%S") %></p>
<% end %>
<%= button_to "Run Import", retry_import_admin_conference_path(@conference), method: :post, class: "mt-2 bg-yellow-500 hover:bg-yellow-700 text-white font-bold py-2 px-4 rounded" %>
<%= button_to "Run Import", retry_import_admin_conference_path(@conference), method: :post, class: "mt-2 btn btn-primary" %>
</div>
<% else %>
<div class="flex justify-between items-center mb-6">
......@@ -65,7 +65,7 @@
<% end %>
</div>
<%= button_to "Run Import Again", retry_import_admin_conference_path(@conference), method: :post, class: "px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600" %>
<%= button_to "Run Import Again", retry_import_admin_conference_path(@conference), method: :post, class: "btn btn-secondary" %>
</div>
<%= form_with(model: [:admin, @conference], url: update_relevant_stages_admin_conference_path(@conference), method: :patch, class: "space-y-6") do |form| %>
......@@ -85,8 +85,8 @@
</div>
<div class="flex justify-between pt-6">
<%= link_to "Back to Conferences", 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 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600" %>
<%= form.submit "Save Relevant Stages", 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" %>
<%= link_to "Back to Conferences", admin_conferences_path, class: "btn btn-tertiary" %>
<%= form.submit "Save Relevant Stages", class: "btn btn-primary" %>
</div>
<% end %>
<% end %>
......
<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">Conference: <%= @conference.name %></h1>
<div class="flex space-x-2">
<%= link_to "Edit", edit_admin_conference_path(@conference), class: "btn btn-primary" %>
<%= link_to "Back to List", admin_conferences_path, class: "btn btn-tertiary" %>
</div>
</div>
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-4">Details</h2>
<div class="space-y-4">
<div>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Name</h3>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-200"><%= @conference.name %></p>
</div>
<div>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Slug</h3>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-200"><%= @conference.slug %></p>
</div>
<div>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">URL</h3>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-200">
<a href="<%= @conference.url %>" target="_blank" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
<%= @conference.url %>
</a>
</p>
</div>
<div>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Time Zone</h3>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-200"><%= @conference.time_zone || 'Not set' %></p>
</div>
<div>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Dates</h3>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-200">
<%= @conference.starts_at&.strftime('%Y-%m-%d %H:%M') %> to <%= @conference.ends_at&.strftime('%Y-%m-%d %H:%M') %>
</p>
</div>
<div>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Import Job Class</h3>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-200"><%= @conference.import_job_class || 'Not set' %></p>
</div>
</div>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-4">Statistics</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Stages</h3>
<p class="mt-1 text-xl font-semibold text-gray-900 dark:text-gray-200"><%= @conference.stages.count %></p>
</div>
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Sessions</h3>
<p class="mt-1 text-xl font-semibold text-gray-900 dark:text-gray-200"><%= @conference.sessions.count %></p>
</div>
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Speakers</h3>
<p class="mt-1 text-xl font-semibold text-gray-900 dark:text-gray-200"><%= @conference.speakers.count %></p>
</div>
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Assignments</h3>
<p class="mt-1 text-xl font-semibold text-gray-900 dark:text-gray-200"><%= @conference.sessions.joins(:assignments).count %></p>
</div>
</div>
<div class="mt-6">
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-4">Import Status</h2>
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<div class="flex items-center justify-between">
<div>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Last Import</h3>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-200">
<% if @conference.last_import_completed_at %>
<%= @conference.last_import_completed_at.strftime('%Y-%m-%d %H:%M:%S') %>
<% else %>
Never completed
<% end %>
</p>
</div>
<div class="flex space-x-2">
<%= link_to "Import History", import_history_admin_conference_path(@conference), class: "btn btn-info" %>
<%= button_to "Run Import Now", retry_import_admin_conference_path(@conference), method: :post, class: "btn btn-success" %>
</div>
</div>
</div>
</div>
</div>
</div>
<% if @conference.data.present? %>
<div class="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-4">Data</h2>
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<pre class="text-xs text-gray-800 dark:text-gray-200 overflow-auto max-h-64"><%= JSON.pretty_generate(@conference.data) %></pre>
</div>
</div>
<% end %>
</div>
</div>
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold dark:text-gray-200 mb-6">Admin Dashboard</h1>
<!-- Stats Overview -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg">
<div class="px-4 py-5 sm:p-6">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Conferences</dt>
<dd class="mt-1 text-3xl font-semibold text-blue-600 dark:text-blue-400">
<%= link_to @conferences_count, admin_conferences_path, class: "hover:underline" %>
</dd>
</dl>
</div>
</div>
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg">
<div class="px-4 py-5 sm:p-6">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Sessions</dt>
<dd class="mt-1 text-3xl font-semibold text-blue-600 dark:text-blue-400">
<%= @sessions_count %>
</dd>
</dl>
</div>
</div>
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg">
<div class="px-4 py-5 sm:p-6">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Users</dt>
<dd class="mt-1 text-3xl font-semibold text-blue-600 dark:text-blue-400">
<%= link_to @users_count, admin_users_path, class: "hover:underline" %>
</dd>
</dl>
</div>
</div>
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg">
<div class="px-4 py-5 sm:p-6">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Assignments</dt>
<dd class="mt-1 text-3xl font-semibold text-blue-600 dark:text-blue-400">
<%= @assignments_count %>
</dd>
</dl>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Recent Imports -->
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Recent Import Jobs</h3>
</div>
<div class="px-4 py-5 sm:p-6">
<% if @recent_imports.any? %>
<div class="flow-root">
<ul class="-my-5 divide-y divide-gray-200 dark:divide-gray-700">
<% @recent_imports.each do |import| %>
<li class="py-4">
<div class="flex items-center space-x-4">
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">
<%= link_to import.conference.name, admin_conference_path(import.conference), class: "text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300" %>
<% status_class = case import.status
when 'completed' then 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
when 'failed' then 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
else 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
end %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <%= status_class %>">
<%= import.status %>
</span>
</p>
<p class="text-sm text-gray-500 dark:text-gray-400">
Started <%= import.started_at.strftime("%Y-%m-%d %H:%M") %>
<% if import.completed_at %>
Completed <%= import.completed_at.strftime("%Y-%m-%d %H:%M") %>
<% end %>
</p>
</div>
<div>
<% if import.conference %>
<%= link_to "Details", import_history_admin_conference_path(import.conference), class: "text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300" %>
<% end %>
</div>
</div>
</li>
<% end %>
</ul>
</div>
<% else %>
<p class="text-gray-500 dark:text-gray-400">No recent imports found.</p>
<% end %>
</div>
</div>
<!-- User Roles -->
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Users with Special Roles</h3>
</div>
<div class="px-4 py-5 sm:p-6">
<div class="space-y-6">
<div>
<h4 class="font-medium text-gray-900 dark:text-white">Admins</h4>
<div class="mt-2">
<% if @admin_users.any? %>
<div class="flex flex-wrap gap-2">
<% @admin_users.each do |user| %>
<%= link_to admin_user_path(user), class: "inline-flex items-center px-3 py-1 rounded-full text-sm bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200" do %>
<div class="w-5 h-5 rounded-full mr-2" style="background-color: <%= user.avatar_color %>"></div>
<%= user.name %>
<% end %>
<% end %>
</div>
<% else %>
<p class="text-gray-500 dark:text-gray-400">No admin users found.</p>
<% end %>
</div>
</div>
<div>
<h4 class="font-medium text-gray-900 dark:text-white">Events Admins</h4>
<div class="mt-2">
<% if @events_admins.any? %>
<div class="flex flex-wrap gap-2">
<% @events_admins.each do |user| %>
<%= link_to admin_user_path(user), class: "inline-flex items-center px-3 py-1 rounded-full text-sm bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200" do %>
<div class="w-5 h-5 rounded-full mr-2" style="background-color: <%= user.avatar_color %>"></div>
<%= user.name %>
<% end %>
<% end %>
</div>
<% else %>
<p class="text-gray-500 dark:text-gray-400">No events admin users found.</p>
<% end %>
</div>
</div>
<div>
<h4 class="font-medium text-gray-900 dark:text-white">Shift Coordinators</h4>
<div class="mt-2">
<% if @shift_coordinators.any? %>
<div class="flex flex-wrap gap-2">
<% @shift_coordinators.each do |user| %>
<%= link_to admin_user_path(user), class: "inline-flex items-center px-3 py-1 rounded-full text-sm bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200" do %>
<div class="w-5 h-5 rounded-full mr-2" style="background-color: <%= user.avatar_color %>"></div>
<%= user.name %>
<% end %>
<% end %>
</div>
<% else %>
<p class="text-gray-500 dark:text-gray-400">No shift coordinator users found.</p>
<% end %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%= 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="columns-1 md:columns-2 gap-6 space-y-6">
<!-- Role Information -->
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg break-inside-avoid mb-6">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Role Information</h3>
</div>
<div class="px-4 py-5 sm:p-6 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>
</div>
</div>
<!-- Permissions -->
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg break-inside-avoid mb-6">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Permissions</h3>
</div>
<div class="px-4 py-5 sm:p-6">
<div class="space-y-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>
</div>
</div>
</div>
<div class="flex justify-between pt-6">
<%= link_to "Cancel", admin_roles_path, class: "btn btn-secondary" %>
<%= form.submit submit_text, class: "btn btn-primary" %>
</div>
<% end %>
<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 %>
<%= render "form", role: @role, permissions: @permissions, submit_text: "Update Role" %>
</div>
<%= form_with(model: [:admin, user], class: "space-y-6") do |form| %>
<% if user.errors.any? %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4 dark:bg-red-900 dark:border-red-700 dark:text-red-300">
<h2 class="font-bold"><%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul class="list-disc list-inside mt-2">
<% user.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="columns-1 md:columns-2 gap-6 space-y-6">
<!-- Account Information -->
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg break-inside-avoid mb-6">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Account Information</h3>
</div>
<div class="px-4 py-5 sm:p-6 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-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :email, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.email_field :email, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :password, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.password_field :password, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
<% if user.persisted? %>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Leave blank if you don't want to change it</p>
<% end %>
</div>
<div>
<%= form.label :password_confirmation, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.password_field :password_confirmation, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<% if user.new_record? %>
<div>
<%= form.label :invitation_token, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :invitation_token, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white", value: "gargamel" %>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Default invitation token</p>
</div>
<% end %>
</div>
</div>
<!-- Communication Channels -->
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg break-inside-avoid mb-6">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Communication Channels</h3>
</div>
<div class="px-4 py-5 sm:p-6 space-y-4">
<div>
<%= form.label :telegram_username, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :telegram_username, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
</div>
</div>
<!-- Languages -->
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg break-inside-avoid mb-6">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Languages</h3>
</div>
<div class="px-4 py-5 sm:p-6 space-y-4">
<div>
<%= form.label :languages_from, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :languages_from, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Comma-separated two-letter codes (e.g., en,de,fr)</p>
</div>
<div>
<%= form.label :languages_to, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :languages_to, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Comma-separated two-letter codes (e.g., en,de,fr)</p>
</div>
</div>
</div>
<!-- Roles -->
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg break-inside-avoid mb-6">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Roles</h3>
</div>
<div class="px-4 py-5 sm:p-6">
<div class="space-y-4">
<% Role.all.each do |role| %>
<div class="flex items-start">
<div class="flex items-center h-5">
<%= check_box_tag "user[role_ids][]", role.id, user.roles.include?(role), id: "user_role_#{role.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" %>
</div>
<div class="ml-3 text-sm">
<%= label_tag "user_role_#{role.id}", role.name, class: "font-medium text-gray-700 dark:text-gray-300" %>
<p class="text-gray-500 dark:text-gray-400"><%= role.description %></p>
</div>
</div>
<% end %>
</div>
<%= hidden_field_tag "user[role_ids][]", "" %>
</div>
</div>
</div>
<% end %>
<div class="flex justify-between pt-6">
<div>
<%= link_to "Cancel", admin_users_path, class: "btn btn-secondary" %>
</div>
<div>
<%= submit_tag "Save User", class: "btn btn-primary" %>
</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">Edit User</h1>
<div class="flex space-x-2">
<%= link_to "View User", admin_user_path(@user), class: "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-800" %>
<%= link_to "Back to Users", admin_users_path, class: "px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700 dark:bg-gray-700 dark:hover:bg-gray-800" %>
</div>
</div>
<%= render 'form', user: @user %>
</div>