diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 8e1ab0e1be5f38ed361c8ce0e602c6284956c9b4..1a066d8457d49e36a737faa4f32f84bb389d6940 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -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; } diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..681054ea98fb779edc2adbab3373da955f366b2a --- /dev/null +++ b/app/controllers/admin/base_controller.rb @@ -0,0 +1,12 @@ +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 diff --git a/app/controllers/admin/conferences_controller.rb b/app/controllers/admin/conferences_controller.rb index 53ea681424ce4e013a94250406a020cd2bddadbb..437bcf1e199e06921f9a98ba49d1f7443f10fb5c 100644 --- a/app/controllers/admin/conferences_controller.rb +++ b/app/controllers/admin/conferences_controller.rb @@ -1,6 +1,5 @@ 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 ] diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..6f1dbeed0da6d7ed802060c4555ff31fa9478d53 --- /dev/null +++ b/app/controllers/admin/dashboard_controller.rb @@ -0,0 +1,14 @@ +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 diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb index 358b384d222531b9101ee2c5a7d916f007478f63..e1909dd640766731fabc6561bfb1c51466e7c065 100644 --- a/app/controllers/admin/roles_controller.rb +++ b/app/controllers/admin/roles_controller.rb @@ -1,6 +1,5 @@ module Admin - class RolesController < ApplicationController - before_action :authenticate_user! + class RolesController < Admin::BaseController before_action :authorize_role before_action :set_role, only: [ :edit, :update ] diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..6b51e7f60f71b8f08e86c5ea56a327e6d7490bf2 --- /dev/null +++ b/app/controllers/admin/users_controller.rb @@ -0,0 +1,75 @@ +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 diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..f4ed9b643a6906e993e226713ffcb0089b1f52c1 --- /dev/null +++ b/app/helpers/button_helper.rb @@ -0,0 +1,33 @@ +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 diff --git a/app/models/import_history.rb b/app/models/import_history.rb index 3989620580bf7f36f4573a8390d89c30f681dc6b..c51b91ab407d56c51f11b6fa25168f8d4b1e187e 100644 --- a/app/models/import_history.rb +++ b/app/models/import_history.rb @@ -1,7 +1,11 @@ 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 diff --git a/app/views/admin/conferences/_form.html.erb b/app/views/admin/conferences/_form.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..63b52d4ea2bb7027fb98224defb866fd107897d1 --- /dev/null +++ b/app/views/admin/conferences/_form.html.erb @@ -0,0 +1,195 @@ +<%= 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 %> diff --git a/app/views/admin/conferences/edit.html.erb b/app/views/admin/conferences/edit.html.erb index 3f45d1144dedfef786c3e8acb515306c25650270..937d5c59691ab0b85fb10dc7a84dd9b39b3c8d82 100644 --- a/app/views/admin/conferences/edit.html.erb +++ b/app/views/admin/conferences/edit.html.erb @@ -1,194 +1,5 @@ <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> diff --git a/app/views/admin/conferences/import_history.html.erb b/app/views/admin/conferences/import_history.html.erb index c481d63a257fb436e29675fcbc30003553ad3d24..f8e0567a5feb6f09528491a3d4fc738f017bc995 100644 --- a/app/views/admin/conferences/import_history.html.erb +++ b/app/views/admin/conferences/import_history.html.erb @@ -1,10 +1,10 @@ <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> diff --git a/app/views/admin/conferences/index.html.erb b/app/views/admin/conferences/index.html.erb index 6b0ead04889aec8b1ac050274abbd64bae5a8b26..1dfbf89fbe6a9dfe997fa5a2398f6c6593e3df92 100644 --- a/app/views/admin/conferences/index.html.erb +++ b/app/views/admin/conferences/index.html.erb @@ -1,7 +1,7 @@ <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" %> diff --git a/app/views/admin/conferences/new.html.erb b/app/views/admin/conferences/new.html.erb index b1796ba613aa3a9e1eef5de72ce94c33ea8e9ccd..b285d92893487cecd629310a32c8b6eb572eda21 100644 --- a/app/views/admin/conferences/new.html.erb +++ b/app/views/admin/conferences/new.html.erb @@ -1,113 +1,5 @@ <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> diff --git a/app/views/admin/conferences/select_relevant_stages.html.erb b/app/views/admin/conferences/select_relevant_stages.html.erb index 3eb8c3c980f996e91f75747d84eae9b59830580d..b4bacf4e2dbe3c1fde6d786a4942b0adb3ae4287 100644 --- a/app/views/admin/conferences/select_relevant_stages.html.erb +++ b/app/views/admin/conferences/select_relevant_stages.html.erb @@ -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 %> diff --git a/app/views/admin/conferences/show.html.erb b/app/views/admin/conferences/show.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..285994e6c13d50d78f464f7bd58924d8011be356 --- /dev/null +++ b/app/views/admin/conferences/show.html.erb @@ -0,0 +1,114 @@ +<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> diff --git a/app/views/admin/dashboard/index.html.erb b/app/views/admin/dashboard/index.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..2622d66cdbd2c32b0cdc5d5c69d4ac4b1ad13bc8 --- /dev/null +++ b/app/views/admin/dashboard/index.html.erb @@ -0,0 +1,163 @@ +<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-indigo-600 dark:text-indigo-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-indigo-600 dark:text-indigo-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-indigo-600 dark:text-indigo-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-indigo-600 dark:text-indigo-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-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-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-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-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> diff --git a/app/views/admin/roles/_form.html.erb b/app/views/admin/roles/_form.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..e252231af7621c57d34683bb0e139d5cd490dcb1 --- /dev/null +++ b/app/views/admin/roles/_form.html.erb @@ -0,0 +1,58 @@ +<%= 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 %> diff --git a/app/views/admin/roles/edit.html.erb b/app/views/admin/roles/edit.html.erb index b07daa46228f8e5df8412b3c81fbdb5090a5a40b..ce796b2c5b84303170213d226ca96369100ec876 100644 --- a/app/views/admin/roles/edit.html.erb +++ b/app/views/admin/roles/edit.html.erb @@ -1,50 +1,5 @@ <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> diff --git a/app/views/admin/users/_form.html.erb b/app/views/admin/users/_form.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..893924e3d72124d476a650e0323b203f13484130 --- /dev/null +++ b/app/views/admin/users/_form.html.erb @@ -0,0 +1,118 @@ +<%= 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> diff --git a/app/views/admin/users/edit.html.erb b/app/views/admin/users/edit.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..987cb1f7579a710e7487e3a9d91b9d7c439a072b --- /dev/null +++ b/app/views/admin/users/edit.html.erb @@ -0,0 +1,11 @@ +<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> diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..0168ea3d1c16be36d9fe83d3e6e3cddcc2e915ec --- /dev/null +++ b/app/views/admin/users/index.html.erb @@ -0,0 +1,45 @@ +<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: Users</h1> + <%= link_to "New User", new_admin_user_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">Email</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">Roles</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">Languages From</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">Languages To</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"> + <% @users.each do |user| %> + <tr> + <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-200"><%= user.name %></td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= user.email %></td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"> + <% user.roles.each do |role| %> + <span class="inline-block bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs mr-1 mb-1 dark:bg-blue-900 dark:text-blue-300"><%= role.name %></span> + <% end %> + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= user.languages_from %></td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= user.languages_to %></td> + <td class="px-6 py-4 whitespace-nowrap text-sm font-medium"> + <div class="flex space-x-2"> + <%= link_to "View", admin_user_path(user), class: "text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300" %> + <%= link_to "Edit", edit_admin_user_path(user), class: "text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300" %> + <%= button_to "Delete", admin_user_path(user), method: :delete, + data: { confirm: "Are you sure you want to delete #{user.name}?" }, + 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> diff --git a/app/views/admin/users/new.html.erb b/app/views/admin/users/new.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..086351a4e39c839c671f23ecf0213234590f05f8 --- /dev/null +++ b/app/views/admin/users/new.html.erb @@ -0,0 +1,8 @@ +<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">New User</h1> + <%= 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> + + <%= render 'form', user: @user %> +</div> diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..961e449641d2157ed040a5ca1b7c1ec96f1ed9eb --- /dev/null +++ b/app/views/admin/users/show.html.erb @@ -0,0 +1,50 @@ +<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">User Details</h1> + <div class="flex space-x-2"> + <%= link_to "Edit", edit_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> + + <div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg divide-y divide-gray-200 dark:divide-gray-700"> + <!-- Basic User Info --> + <div class="px-6 py-4"> + <div class="flex items-center"> + <div class="w-12 h-12 rounded-full flex items-center justify-center mr-4" style="background-color: <%= @user.avatar_color %>; color: <%= @user.text_color %>"> + <span class="text-xl font-medium"><%= @user.initials %></span> + </div> + <div> + <h3 class="text-lg font-medium text-gray-900 dark:text-white"><%= @user.name %></h3> + <p class="text-sm text-gray-500 dark:text-gray-400"><%= @user.email %></p> + <% if @user.roles.any? %> + <div class="flex flex-wrap gap-2 mt-2"> + <% @user.roles.each do |role| %> + <span class="inline-block bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs mr-1 mb-1 dark:bg-blue-900 dark:text-blue-300"><%= role.name %></span> + <% end %> + </div> + <% else %> + <p class="text-sm text-gray-700 dark:text-gray-300">No roles assigned</p> + <% end %> + </div> + </div> + </div> + + <!-- Communication --> + <div class="px-6 py-4"> + <h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Communication</h4> + <div class="text-sm text-gray-700 dark:text-gray-300"> + <p><strong>Telegram:</strong> <%= @user.telegram_username.presence || "Not set" %></p> + </div> + </div> + + <!-- Languages --> + <div class="px-6 py-4"> + <h4 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Languages</h4> + <div class="space-y-2 text-sm text-gray-700 dark:text-gray-300"> + <p><strong>From:</strong> <%= @user.languages_from.presence || "Not set" %></p> + <p><strong>To:</strong> <%= @user.languages_to.presence || "Not set" %></p> + </div> + </div> + </div> +</div> diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index 5fa03b782af2ea8e40716efc98d77c7ceecfeecb..3c5be50a85e59cb23754760ccc064a887819cc24 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -1,83 +1,86 @@ -<div> - -<h1 class="text-xl my-4 dark:text-red-500">Profile</h1> - -<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> - <%= render "devise/shared/error_messages", resource: resource %> - - <div class="field"> - <%= f.label :name %> - <%= f.text_field :name, autofocus: true, autocomplete: "username" %> - </div> - - <div class="field hidden"> - <%= f.label :email %> - <%= f.email_field :email, autocomplete: "email" %> - </div> - - <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> - <div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div> - <% end %> - - <div class="field"> - <%= f.label :password %> - <i class="block">(leave blank if you don't want to change it)</i> - <%= f.password_field :password, autocomplete: "new-password" %> - <% if @minimum_password_length %> - <em><%= @minimum_password_length %> characters minimum</em> +<div class="container mx-auto px-4 py-8"> + <div class="max-w-2xl mx-auto bg-white dark:bg-gray-800 shadow rounded-lg p-6"> + <h1 class="text-2xl font-bold mb-6 dark:text-white">Profile</h1> + + <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + + <div class="mb-4"> + <%= f.label :name, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.text_field :name, autofocus: true, autocomplete: "username", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> + + <div class="hidden"> + <%= f.label :email, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.email_field :email, autocomplete: "email", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> + + <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> + <div class="mb-4 p-3 bg-yellow-100 border border-yellow-400 text-yellow-700 rounded"> + Currently waiting confirmation for: <%= resource.unconfirmed_email %> + </div> + <% end %> + + <div class="mb-4"> + <%= f.label :password, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <p class="text-sm text-gray-500 dark:text-gray-400 mb-2">(leave blank if you don't want to change it)</p> + <%= f.password_field :password, autocomplete: "new-password", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + <% if @minimum_password_length %> + <p class="text-sm text-gray-500 dark:text-gray-400 mt-1"><%= @minimum_password_length %> characters minimum</p> + <% end %> + </div> + + <div class="mb-4"> + <%= f.label :password_confirmation, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.password_field :password_confirmation, autocomplete: "new-password", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> + + <div class="mb-4"> + <%= f.label :darkmode, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.select :darkmode, User.darkmodes.keys.map { |d| [d.humanize, d] }, {}, class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> + + <div class="mb-4"> + <%= f.label :avatar_color, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.color_field :avatar_color, class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500" %> + </div> + + <div class="hidden"> + <%= f.label :telegram_username, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.text_field :telegram_username, class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> + + <fieldset class="mb-4 border border-gray-300 dark:border-gray-600 p-4 rounded-md"> + <legend class="text-lg font-semibold text-gray-700 dark:text-gray-300 px-2">More Languages Team Only</legend> + <p class="text-sm text-gray-500 dark:text-gray-400 mb-2">Please use comma-separated two-letter codes.</p> + <p class="text-sm text-gray-500 dark:text-gray-400 mb-4">Leave empty unless you are with the more languages team.</p> + + <div class="mb-4"> + <%= f.label :languages_from, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.text_field :languages_from, placeholder: "de,en", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> + + <div class="mb-4"> + <%= f.label :languages_to, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.text_field :languages_to, placeholder: "jp,es", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> + </fieldset> + + <div class="mb-6"> + <%= f.label :current_password, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <p class="text-sm text-gray-500 dark:text-gray-400 mb-2">(we need your current password to confirm your changes)</p> + <%= f.password_field :current_password, autocomplete: "current-password", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> + + <div class="mb-6"> + <%= f.submit "Update Profile", class: "w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-blue-700 dark:hover:bg-blue-800" %> + </div> <% end %> - </div> - - <div class="field"> - <%= f.label :password_confirmation %> - <%= f.password_field :password_confirmation, autocomplete: "new-password" %> - </div> - - <div class="field"> - <%= f.label :darkmode %> - <%= f.select :darkmode, User.darkmodes.keys.map { |d| [d.humanize, d] } %> - </div> - - <div class="field"> - <%= f.label :avatar_color %> - <%= f.color_field :avatar_color %> - </div> - <div class="field hidden"> - <%= f.label :telegram_username %> - <%= f.text_field :telegram_username %> - </div> + <h2 class="hidden">Cancel my account</h2> -<fieldset class="border border-gray-300 p-4 rounded-md"> - <legend class="text-lg font-semibold">More Languages Team Only</legend> - <i class="block">Please use comma-separated two-letter codes.</i> - <i class="block">Leave empty unless you are with the more languages team.</i> - - <div class="field"> - <%= f.label :languages_from %> - <%= f.text_field :languages_from, placeholder: "de,en" %> - </div> - - <div class="field"> - <%= f.label :languages_to %> - <%= f.text_field :languages_to, placeholder: "jp,es" %> - </div> - </fieldset> - - <div class="field"> - <%= f.label :current_password %> - <i class="block">(we need your current password to confirm your changes)</i> - <%= f.password_field :current_password, autocomplete: "current-password" %> - </div> + <div class="hidden">Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete %></div> - <div class="actions"> - <%= f.submit "Update Profile" %> + <%= link_to "Back", :back, class: "inline-block text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 mt-4" %> </div> -<% end %> - -<h2 class="hidden">Cancel my account</h2> - -<div class="hidden">Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete %></div> - -<%= link_to "Back", :back, class:"block mt-4" %> </div> diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index b532322b03529d3c772b9ba205e2ad29f27337e4..9d236c9dd18a7daa21a69a93d12eb5b4928db614 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -1,36 +1,38 @@ -<div> -<h1 class="text-xl my-4 dark:text-red-500">Sign Up</h1> +<div class="container mx-auto px-4 py-8"> + <div class="max-w-md mx-auto bg-white dark:bg-gray-800 shadow rounded-lg p-6"> + <h1 class="text-2xl font-bold mb-6 dark:text-white">Sign Up</h1> -<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> - <%= render "devise/shared/error_messages", resource: resource %> + <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> - <div class="field"> - <%= f.label :name %> - <%= f.text_field :name, autofocus: true, autocomplete: "username" %> - </div> + <div class="mb-4"> + <%= f.label :name, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.text_field :name, autofocus: true, autocomplete: "username", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> - <div class="field"> - <%= f.label :password %> - <% if @minimum_password_length %> - <em class="block">(<%= @minimum_password_length %> characters minimum)</em> - <% end %> - <%= f.password_field :password, autocomplete: "new-password" %> - </div> + <div class="mb-4"> + <%= f.label :password, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <% if @minimum_password_length %> + <p class="text-sm text-gray-500 dark:text-gray-400 mb-2">(<%= @minimum_password_length %> characters minimum)</p> + <% end %> + <%= f.password_field :password, autocomplete: "new-password", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> - <div class="field"> - <%= f.label :password_confirmation %> - <%= f.password_field :password_confirmation, autocomplete: "new-password" %> - </div> + <div class="mb-4"> + <%= f.label :password_confirmation, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.password_field :password_confirmation, autocomplete: "new-password", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> - <div class="field"> - <%= f.label :invitation_token, "Invitation Token" %> - <%= f.text_field :invitation_token, autocomplete: "off" %> - </div> + <div class="mb-4"> + <%= f.label :invitation_token, "Invitation Token", class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.text_field :invitation_token, autocomplete: "off", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> - <div class="actions"> - <%= f.submit "Sign up" %> - </div> -<% end %> + <div class="mb-6"> + <%= f.submit "Sign up", class: "w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-blue-700 dark:hover:bg-blue-800" %> + </div> + <% end %> -<%= render "devise/shared/links" %> + <%= render "devise/shared/links" %> + </div> </div> diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index 70ca0530173d0ca5699ef60e32786f84287311d5..7d0913132097c856cfef19548b141d34937f6358 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -1,28 +1,32 @@ -<div> -<h1 class="text-xl my-4 dark:text-red-500">Log in</h1> +<div class="container mx-auto px-4 py-8"> + <div class="max-w-md mx-auto bg-white dark:bg-gray-800 shadow rounded-lg p-6"> + <h1 class="text-2xl font-bold mb-6 dark:text-white">Log in</h1> -<%= form_for(resource, as: resource_name, url: user_session_path) do |f| %> - <div class="field"> - <%= f.label :name %> - <%= f.text_field :name, autofocus: true, autocomplete: "username" %> - </div> + <%= form_for(resource, as: resource_name, url: user_session_path) do |f| %> + <div class="mb-4"> + <%= f.label :name, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.text_field :name, autofocus: true, autocomplete: "username", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> - <div class="field"> - <%= f.label :password %> - <%= f.password_field :password, autocomplete: "current-password" %> - </div> + <div class="mb-4"> + <%= f.label :password, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.password_field :password, autocomplete: "current-password", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> - <% if devise_mapping.rememberable? %> - <div class="field"> - <%= f.check_box :remember_me %> - <%= f.label :remember_me, class: "!inline-block align-middle" %> - </div> - <% end %> + <% if devise_mapping.rememberable? %> + <div class="mb-4"> + <div class="flex items-center"> + <%= f.check_box :remember_me, class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" %> + <%= f.label :remember_me, class: "ml-2 block text-gray-700 dark:text-gray-300" %> + </div> + </div> + <% end %> - <div class="actions"> - <%= f.submit "Log in" %> - </div> -<% end %> + <div class="mb-6"> + <%= f.submit "Log in", class: "w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-blue-700 dark:hover:bg-blue-800" %> + </div> + <% end %> -<%= render "devise/shared/links" %> + <%= render "devise/shared/links" %> + </div> </div> diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb index 54410c523f7afd8d050e3ec7ec9252672585eaa7..9486dc88fed81bb4aff4fc1742e0e63edfcfdb84 100644 --- a/app/views/devise/shared/_links.html.erb +++ b/app/views/devise/shared/_links.html.erb @@ -1,27 +1,29 @@ -<div class="pt-4"> -<%- if controller_name != 'sessions' %> - <%= link_to "Log in", new_user_session_path %><br /> -<% end %> +<div class="pt-6 space-y-2"> + <%- if controller_name != 'sessions' %> + <%= link_to "Log in", new_user_session_path, class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" %> + <% end %> -<%- if devise_mapping.registerable? && controller_name != 'registrations' %> - <%= link_to "Sign up", new_registration_path(resource_name) %><br /> -<% end %> + <%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to "Sign up", new_registration_path(resource_name), class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 block" %> + <% end %> -<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> - <%= link_to "Forgot your password?", new_password_path(resource_name) %><br /> -<% end %> + <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> + <%= link_to "Forgot your password?", new_password_path(resource_name), class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 block" %> + <% end %> -<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> - <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br /> -<% end %> + <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> + <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name), class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 block" %> + <% end %> -<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> - <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br /> -<% end %> + <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> + <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name), class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 block" %> + <% end %> -<%- if devise_mapping.omniauthable? %> - <%- resource_class.omniauth_providers.each do |provider| %> - <%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), data: { turbo: false } %><br /> + <%- if devise_mapping.omniauthable? %> + <%- resource_class.omniauth_providers.each do |provider| %> + <%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), + class: "px-4 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 rounded-md mt-2", + data: { turbo: false } %> + <% end %> <% end %> -<% end %> </div> diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..205267410192e9c86afdfac94d09860da442a32f --- /dev/null +++ b/app/views/layouts/admin.html.erb @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html> + <head> + <title>Admin - re:scheduled</title> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %> + <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> + <%= javascript_importmap_tags %> + </head> + + <body class="bg-gray-100 dark:bg-gray-900 min-h-screen"> + <%= render 'shared/admin_nav' %> + + <main> + <% if notice %> + <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4 mx-4 mt-4" role="alert"> + <span class="block sm:inline"><%= notice %></span> + </div> + <% end %> + + <% if alert %> + <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4 mx-4 mt-4" role="alert"> + <span class="block sm:inline"><%= alert %></span> + </div> + <% end %> + + <%= yield %> + </main> + </body> +</html> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index cf4a9afe6f212966f302723c733dd9066e003a30..6f042fa7b7a6d486929c536a31740944e658d8c5 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html> +<html class="<%= "dark" if current_user&.darkmode == "dark"%>"> <head> <title>re:scheduled</title> <meta name="viewport" content="width=device-width,initial-scale=1"> @@ -11,7 +11,7 @@ <%= javascript_importmap_tags %> </head> - <body class="bg-gray-100 dark:bg-gray-900 min-h-screen"> + <body <%= tag.attributes(body_data_attributes) %> class="bg-gray-100 dark:bg-gray-900 min-h-screen"> <header class="bg-white dark:bg-gray-800 shadow"> <div class="container mx-auto px-4 py-4 flex justify-between items-center"> <div class="flex items-center"> @@ -19,22 +19,29 @@ <%= link_to "re:scheduled", root_path %> </h1> </div> + <nav class="flex items-center space-x-4"> <% if user_signed_in? %> - <% if current_user.has_role?("events_admin") %> - <%= link_to "Admin", admin_conferences_path, class: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" %> - <% end %> - <%= link_to "Conferences", conferences_path, class: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" %> - <%= link_to "Assignments", assignments_path, class: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" %> - <%= link_to "Sign Out", destroy_user_session_path, method: :delete, class: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" %> - <div class="flex items-center mr-4"> - <div class="w-8 h-8 rounded-full flex items-center justify-center mr-2" style="background-color: <%= current_user.avatar_color %>; color: <%= current_user.text_color %>"> - <%= current_user.initials %> - </div> - <span class="text-gray-700 dark:text-gray-300"><%= current_user.name %></span> + <div class="flex items-center space-x-4 ml-4"> + <%= render partial: "application/user_avatar", locals: { user: current_user } %> + <%= link_to edit_user_registration_path, class: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white", aria_label: "My Profile" do %> + <span class="hidden lg:inline">My </span>Profile + <% end %> + <%= link_to user_assignments_path(current_user), class: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" do %> + <span class="hidden lg:inline">My </span>Assignments + <% end %> + <%= link_to "Logout", destroy_user_session_path, data: { turbo_method: :delete }, class: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" %> + + <% if current_user.has_role?("admin") || current_user.has_role?("events_admin") %> + <%= link_to "Admin", admin_root_path, class: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" %> + <% end %> </div> <% else %> - <%= link_to "Sign In", new_user_session_path, class: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" %> + <div class="flex items-center space-x-4"> + <span class="px-2 text-gray-600 dark:text-gray-400">not logged in</span> + <%= link_to "Login", new_user_session_path, class: "hover:text-gray-900 dark:hover:text-slate-200" %> + <%= link_to "Sign Up", new_user_registration_path, class: "hover:text-gray-900 dark:hover:text-slate-200" %> + </div> <% end %> </nav> </div> diff --git a/app/views/shared/_admin_nav.html.erb b/app/views/shared/_admin_nav.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..dc1ef3fe57dcc9748aa2cbeb7df08718e9663a16 --- /dev/null +++ b/app/views/shared/_admin_nav.html.erb @@ -0,0 +1,21 @@ +<header class="bg-gray-800 text-white shadow"> + <div class="container mx-auto px-4 py-4 flex justify-between items-center"> + <div class="flex items-center"> + <h1 class="text-xl font-bold"> + <%= link_to "re:scheduled admin", admin_root_path %> + </h1> + </div> + <nav class="flex items-center space-x-4"> + <%= link_to admin_user_path(current_user), class: "flex items-center ml-4 text-gray-300 hover:text-white" do %> + <%= render partial: "application/user_avatar", locals: { user: current_user } %> + <% end %> + + <%= link_to "Dashboard", admin_dashboard_path, class: "text-gray-300 hover:text-white" %> + <%= link_to "Users", admin_users_path, class: "text-gray-300 hover:text-white" %> + <%= link_to "Roles", admin_roles_path, class: "text-gray-300 hover:text-white" %> + <%= link_to "Conferences", admin_conferences_path, class: "text-gray-300 hover:text-white" %> + + <%= link_to "Back to Site", root_path, class: "ml-4 text-gray-300 hover:text-white" %> + </nav> + </div> +</header> diff --git a/config/routes.rb b/config/routes.rb index 9b1c73d259d1bb44701364c8ab201cb4a48dbf91..d1ba65249f9c286fa8ff512c475107cff166e75a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,15 @@ Rails.application.routes.draw do + mount Crono::Engine, at: "/crono" + mount ActionCable.server => "/cable" + + devise_for :users + namespace :admin do + root to: "dashboard#index" + get "dashboard", to: "dashboard#index" + resources :roles, only: [ :index, :edit, :update ] + resources :users resources :conferences, param: :slug do member do get :import_progress @@ -13,9 +22,6 @@ Rails.application.routes.draw do end end end - devise_for :users - mount Crono::Engine, at: "/crono" - mount ActionCable.server => "/cable" get "speakers/show" get "users/leaderboard" diff --git a/db/migrate/20250304101405_migrate_to_rbac_system.rb b/db/migrate/20250304101405_migrate_to_rbac_system.rb index 7d22f9575409251887ee7c000c4891f6f93cafb9..49afc64098387be9bdd635277892cd920274e350 100644 --- a/db/migrate/20250304101405_migrate_to_rbac_system.rb +++ b/db/migrate/20250304101405_migrate_to_rbac_system.rb @@ -1,5 +1,7 @@ class MigrateToRbacSystem < ActiveRecord::Migration[8.0] def up + $stderr.puts "MigrateToRbacSystem" + # Create roles shift_coordinator_role = Role.create!(name: 'shift_coordinator', description: 'Can manage session assignments and scheduling') events_admin_role = Role.create!(name: 'events_admin', description: 'Can manage conferences and all sub-resources') diff --git a/db/migrate/20250308080600_add_admin_role_and_permissions.rb b/db/migrate/20250308080600_add_admin_role_and_permissions.rb new file mode 100644 index 0000000000000000000000000000000000000000..9938411009dece2711b56081b1382ca3a79a4f19 --- /dev/null +++ b/db/migrate/20250308080600_add_admin_role_and_permissions.rb @@ -0,0 +1,43 @@ +class AddAdminRoleAndPermissions < ActiveRecord::Migration[8.0] + def up + # Create admin role + admin_role = Role.create!(name: 'admin', description: 'Can manage users and assign roles') + + # Create user management permissions + manage_users = Permission.create!(name: 'manage_users', description: 'Can create, edit, and delete users') + assign_roles = Permission.create!(name: 'assign_roles', description: 'Can assign roles to users') + + # Associate permissions with admin role + admin_role.permissions << manage_users + admin_role.permissions << assign_roles + + # Also give admin all the permissions of events_admin and shift_coordinator + events_admin_role = Role.find_by(name: 'events_admin') + if events_admin_role + events_admin_role.permissions.each do |permission| + admin_role.permissions << permission unless admin_role.permissions.include?(permission) + end + end + + shift_coordinator_role = Role.find_by(name: 'shift_coordinator') + if shift_coordinator_role + shift_coordinator_role.permissions.each do |permission| + admin_role.permissions << permission unless admin_role.permissions.include?(permission) + end + end + end + + def down + # Find and remove the admin role + admin_role = Role.find_by(name: 'admin') + if admin_role + # First remove all role_permissions associations + admin_role.role_permissions.destroy_all + # Then delete the role itself + admin_role.destroy + end + + # Delete the permissions + Permission.where(name: [ 'manage_users', 'assign_roles' ]).destroy_all + end +end diff --git a/db/schema.rb b/db/schema.rb index 404bf0eda7db7de252c9a734350b3f221832da56..a86dfeb8ca45805123a886d2d5c1147ec50ab282 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_03_04_204807) do +ActiveRecord::Schema[8.0].define(version: 2025_03_08_080600) do create_table "assignments", force: :cascade do |t| t.integer "user_id", null: false t.integer "session_id", null: false diff --git a/db/seeds.rb b/db/seeds.rb index ae39fcb7ef15224a3f1449b9aad729ebbd3024fa..1e1ae76ba025c4f56aaea69e9b4beb2a03df7872 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -49,368 +49,426 @@ # c.save! # end -Conference.find_or_create_by(slug: "38c3").tap do |c| - c.name = "38th Chaos Communication Congress (de-en)" - c.time_zone = "Berlin" - c.starts_at = DateTime.parse("27 December 2024 10:30 CET") - c.ends_at = DateTime.parse("30 December 2024 19:00 CET") - c.data = { - "heartbeat_url" => fetch_credential("heartbeat_deen"), - "schedule_url" => "https://api.events.ccc.de/congress/2024/assembly/6840c453-af5c-413c-8127-adcbdcd98e9e/schedule.json", - "filedrop_url" => "https://speakers.c3lingo.org/", - "engelsystem_url" => "https://engel.events.ccc.de/api/v0-beta/" - } - c.import_job_class = "pretalx" - c.location = "Congress Center Hamburg" - c.save! -end +# Conference.find_or_create_by(slug: "38c3").tap do |c| +# c.name = "38th Chaos Communication Congress (de-en)" +# c.time_zone = "Berlin" +# c.starts_at = DateTime.parse("27 December 2024 10:30 CET") +# c.ends_at = DateTime.parse("30 December 2024 19:00 CET") +# c.data = { +# "heartbeat_url" => fetch_credential("heartbeat_deen"), +# "schedule_url" => "https://api.events.ccc.de/congress/2024/assembly/6840c453-af5c-413c-8127-adcbdcd98e9e/schedule.json", +# "filedrop_url" => "https://speakers.c3lingo.org/", +# "engelsystem_url" => "https://engel.events.ccc.de/api/v0-beta/" +# } +# c.import_job_class = "pretalx" +# c.location = "Congress Center Hamburg" +# c.save! +# end -Conference.find_or_create_by(slug: "38c3-more").tap do |c| - c.name = "38th Chaos Communication Congress (more languages)" - c.more_languages = true - c.time_zone = "Berlin" - c.starts_at = DateTime.parse("27 December 2024 10:30 CET") - c.ends_at = DateTime.parse("30 December 2024 19:00 CET") - c.data = { - "heartbeat_url" => fetch_credential("heartbeat_more"), - "schedule_url" => "https://api.events.ccc.de/congress/2024/assembly/6840c453-af5c-413c-8127-adcbdcd98e9e/schedule.json", - "filedrop_url" => "https://speakers.c3lingo.org/", - "engelsystem_url" => "https://engel.events.ccc.de/api/v0-beta/" - } - c.import_job_class = "pretalx" - c.location = "Congress Center Hamburg" - c.save! -end +# Conference.find_or_create_by(slug: "38c3-more").tap do |c| +# c.name = "38th Chaos Communication Congress (more languages)" +# c.more_languages = true +# c.time_zone = "Berlin" +# c.starts_at = DateTime.parse("27 December 2024 10:30 CET") +# c.ends_at = DateTime.parse("30 December 2024 19:00 CET") +# c.data = { +# "heartbeat_url" => fetch_credential("heartbeat_more"), +# "schedule_url" => "https://api.events.ccc.de/congress/2024/assembly/6840c453-af5c-413c-8127-adcbdcd98e9e/schedule.json", +# "filedrop_url" => "https://speakers.c3lingo.org/", +# "engelsystem_url" => "https://engel.events.ccc.de/api/v0-beta/" +# } +# c.import_job_class = "pretalx" +# c.location = "Congress Center Hamburg" +# c.save! +# end -orga = Conference.find_or_create_by(slug: "38c3-orga").tap do |c| - c.name = "38th Chaos Communication Congress (internal shifts)" - c.time_zone = "Berlin" - c.starts_at = DateTime.parse("27 December 2024 10:30 CET") - c.ends_at = DateTime.parse("30 December 2024 19:00 CET") - c.location = "Congress Center Hamburg" - c.save! -end -stage_standby = Stage.find_or_create_by(conference: orga, ref_id: "c3lingo-stage-deen").tap do |stage_| - stage_.name = "C3Lingo Standby" - stage_.weight = 100 - stage_.save! -end -stage_coordinator = Stage.find_or_create_by(conference: orga, ref_id: "c3lingo-stage-more").tap do |stage_| - stage_.name = "C3Lingo Coordinator" - stage_.weight = 200 - stage_.save! -end -stage_service = Stage.find_or_create_by(conference: orga, ref_id: "c3lingo-stage-service").tap do |stage_| - stage_.name = "C3Lingo Service" - stage_.weight = 50 - stage_.save! -end -orga.relevant_stages = orga.stages -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241227-1015").tap do |shift_| - shift_.stage = stage_standby - shift_.title = "Standby Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-27T10:15:00+01:00" - shift_.ends_at = "2024-12-27T14:15:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241227-1015").tap do |shift_| - shift_.stage = stage_coordinator - shift_.title = "Coordinator Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-27T10:15:00+01:00" - shift_.ends_at = "2024-12-27T14:15:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241228-1015").tap do |shift_| - shift_.stage = stage_standby - shift_.title = "Standby Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-28T10:15:00+01:00" - shift_.ends_at = "2024-12-28T14:15:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241228-1015").tap do |shift_| - shift_.stage = stage_coordinator - shift_.title = "Coordinator Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-28T10:15:00+01:00" - shift_.ends_at = "2024-12-28T14:15:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241229-1015").tap do |shift_| - shift_.stage = stage_standby - shift_.title = "Standby Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-29T10:15:00+01:00" - shift_.ends_at = "2024-12-29T14:15:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241229-1015").tap do |shift_| - shift_.stage = stage_coordinator - shift_.title = "Coordinator Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-29T10:15:00+01:00" - shift_.ends_at = "2024-12-29T14:15:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241230-1015").tap do |shift_| - shift_.stage = stage_standby - shift_.title = "Standby Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-30T10:15:00+01:00" - shift_.ends_at = "2024-12-30T14:15:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241230-1015").tap do |shift_| - shift_.stage = stage_coordinator - shift_.title = "Coordinator Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-30T10:15:00+01:00" - shift_.ends_at = "2024-12-30T14:15:00+01:00" - shift_.save! -end +# orga = Conference.find_or_create_by(slug: "38c3-orga").tap do |c| +# c.name = "38th Chaos Communication Congress (internal shifts)" +# c.time_zone = "Berlin" +# c.starts_at = DateTime.parse("27 December 2024 10:30 CET") +# c.ends_at = DateTime.parse("30 December 2024 19:00 CET") +# c.location = "Congress Center Hamburg" +# c.save! +# end +# stage_standby = Stage.find_or_create_by(conference: orga, ref_id: "c3lingo-stage-deen").tap do |stage_| +# stage_.name = "C3Lingo Standby" +# stage_.weight = 100 +# stage_.save! +# end +# stage_coordinator = Stage.find_or_create_by(conference: orga, ref_id: "c3lingo-stage-more").tap do |stage_| +# stage_.name = "C3Lingo Coordinator" +# stage_.weight = 200 +# stage_.save! +# end +# stage_service = Stage.find_or_create_by(conference: orga, ref_id: "c3lingo-stage-service").tap do |stage_| +# stage_.name = "C3Lingo Service" +# stage_.weight = 50 +# stage_.save! +# end +# orga.relevant_stages = orga.stages +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241227-1015").tap do |shift_| +# shift_.stage = stage_standby +# shift_.title = "Standby Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-27T10:15:00+01:00" +# shift_.ends_at = "2024-12-27T14:15:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241227-1015").tap do |shift_| +# shift_.stage = stage_coordinator +# shift_.title = "Coordinator Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-27T10:15:00+01:00" +# shift_.ends_at = "2024-12-27T14:15:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241228-1015").tap do |shift_| +# shift_.stage = stage_standby +# shift_.title = "Standby Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-28T10:15:00+01:00" +# shift_.ends_at = "2024-12-28T14:15:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241228-1015").tap do |shift_| +# shift_.stage = stage_coordinator +# shift_.title = "Coordinator Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-28T10:15:00+01:00" +# shift_.ends_at = "2024-12-28T14:15:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241229-1015").tap do |shift_| +# shift_.stage = stage_standby +# shift_.title = "Standby Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-29T10:15:00+01:00" +# shift_.ends_at = "2024-12-29T14:15:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241229-1015").tap do |shift_| +# shift_.stage = stage_coordinator +# shift_.title = "Coordinator Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-29T10:15:00+01:00" +# shift_.ends_at = "2024-12-29T14:15:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241230-1015").tap do |shift_| +# shift_.stage = stage_standby +# shift_.title = "Standby Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-30T10:15:00+01:00" +# shift_.ends_at = "2024-12-30T14:15:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241230-1015").tap do |shift_| +# shift_.stage = stage_coordinator +# shift_.title = "Coordinator Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-30T10:15:00+01:00" +# shift_.ends_at = "2024-12-30T14:15:00+01:00" +# shift_.save! +# end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241227-1415").tap do |shift_| - shift_.stage = stage_standby - shift_.title = "Standby Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-27T14:15:00+01:00" - shift_.ends_at = "2024-12-27T18:15:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241227-1415").tap do |shift_| - shift_.stage = stage_coordinator - shift_.title = "Coordinator Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-27T14:15:00+01:00" - shift_.ends_at = "2024-12-27T18:15:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241228-1415").tap do |shift_| - shift_.stage = stage_standby - shift_.title = "Standby Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-28T14:15:00+01:00" - shift_.ends_at = "2024-12-28T18:15:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241228-1415").tap do |shift_| - shift_.stage = stage_coordinator - shift_.title = "Coordinator Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-28T14:15:00+01:00" - shift_.ends_at = "2024-12-28T18:15:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241229-1415").tap do |shift_| - shift_.stage = stage_standby - shift_.title = "Standby Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-29T14:15:00+01:00" - shift_.ends_at = "2024-12-29T18:15:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241229-1415").tap do |shift_| - shift_.stage = stage_coordinator - shift_.title = "Coordinator Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-29T14:15:00+01:00" - shift_.ends_at = "2024-12-29T18:15:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241230-1415").tap do |shift_| - shift_.stage = stage_standby - shift_.title = "Standby Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-30T14:15:00+01:00" - shift_.ends_at = "2024-12-30T18:15:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241230-1415").tap do |shift_| - shift_.stage = stage_coordinator - shift_.title = "Coordinator Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-30T14:15:00+01:00" - shift_.ends_at = "2024-12-30T18:15:00+01:00" - shift_.save! -end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241227-1415").tap do |shift_| +# shift_.stage = stage_standby +# shift_.title = "Standby Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-27T14:15:00+01:00" +# shift_.ends_at = "2024-12-27T18:15:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241227-1415").tap do |shift_| +# shift_.stage = stage_coordinator +# shift_.title = "Coordinator Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-27T14:15:00+01:00" +# shift_.ends_at = "2024-12-27T18:15:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241228-1415").tap do |shift_| +# shift_.stage = stage_standby +# shift_.title = "Standby Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-28T14:15:00+01:00" +# shift_.ends_at = "2024-12-28T18:15:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241228-1415").tap do |shift_| +# shift_.stage = stage_coordinator +# shift_.title = "Coordinator Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-28T14:15:00+01:00" +# shift_.ends_at = "2024-12-28T18:15:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241229-1415").tap do |shift_| +# shift_.stage = stage_standby +# shift_.title = "Standby Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-29T14:15:00+01:00" +# shift_.ends_at = "2024-12-29T18:15:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241229-1415").tap do |shift_| +# shift_.stage = stage_coordinator +# shift_.title = "Coordinator Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-29T14:15:00+01:00" +# shift_.ends_at = "2024-12-29T18:15:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241230-1415").tap do |shift_| +# shift_.stage = stage_standby +# shift_.title = "Standby Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-30T14:15:00+01:00" +# shift_.ends_at = "2024-12-30T18:15:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241230-1415").tap do |shift_| +# shift_.stage = stage_coordinator +# shift_.title = "Coordinator Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-30T14:15:00+01:00" +# shift_.ends_at = "2024-12-30T18:15:00+01:00" +# shift_.save! +# end + + +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241227-1900").tap do |shift_| +# shift_.stage = stage_standby +# shift_.title = "Standby Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-27T19:00:00+01:00" +# shift_.ends_at = "2024-12-27T22:00:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241227-1900").tap do |shift_| +# shift_.stage = stage_coordinator +# shift_.title = "Coordinator Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-27T18:15:00+01:00" +# shift_.ends_at = "2024-12-27T22:00:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241228-1900").tap do |shift_| +# shift_.stage = stage_standby +# shift_.title = "Standby Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-28T19:00:00+01:00" +# shift_.ends_at = "2024-12-28T22:00:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241228-1900").tap do |shift_| +# shift_.stage = stage_coordinator +# shift_.title = "Coordinator Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-28T18:15:00+01:00" +# shift_.ends_at = "2024-12-28T22:00:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241229-1900").tap do |shift_| +# shift_.stage = stage_standby +# shift_.title = "Standby Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-29T19:00:00+01:00" +# shift_.ends_at = "2024-12-29T22:00:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241229-1900").tap do |shift_| +# shift_.stage = stage_coordinator +# shift_.title = "Coordinator Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-29T18:15:00+01:00" +# shift_.ends_at = "2024-12-29T22:00:00+01:00" +# shift_.save! +# end + +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241227-2200").tap do |shift_| +# shift_.stage = stage_standby +# shift_.title = "Standby Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-27T22:00:00+01:00" +# shift_.ends_at = "2024-12-28T01:20:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241227-2200").tap do |shift_| +# shift_.stage = stage_coordinator +# shift_.title = "Coordinator Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-27T22:00:00+01:00" +# shift_.ends_at = "2024-12-28T02:40:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241228-2200").tap do |shift_| +# shift_.stage = stage_standby +# shift_.title = "Standby Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-28T22:00:00+01:00" +# shift_.ends_at = "2024-12-29T01:20:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241228-2200").tap do |shift_| +# shift_.stage = stage_coordinator +# shift_.title = "Coordinator Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-28T22:00:00+01:00" +# shift_.ends_at = "2024-12-29T02:25:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241229-2200").tap do |shift_| +# shift_.stage = stage_standby +# shift_.title = "Standby Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-29T22:00:00+01:00" +# shift_.ends_at = "2024-12-30T01:00:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241229-2200").tap do |shift_| +# shift_.stage = stage_coordinator +# shift_.title = "Coordinator Shift" +# shift_.language = "en" +# shift_.url = "https://rescheduled.c3lingo.org/" +# shift_.starts_at = "2024-12-29T22:00:00+01:00" +# shift_.ends_at = "2024-12-30T02:15:00+01:00" +# shift_.save! +# end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241227-1900").tap do |shift_| - shift_.stage = stage_standby - shift_.title = "Standby Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-27T19:00:00+01:00" - shift_.ends_at = "2024-12-27T22:00:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241227-1900").tap do |shift_| - shift_.stage = stage_coordinator - shift_.title = "Coordinator Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-27T18:15:00+01:00" - shift_.ends_at = "2024-12-27T22:00:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241228-1900").tap do |shift_| - shift_.stage = stage_standby - shift_.title = "Standby Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-28T19:00:00+01:00" - shift_.ends_at = "2024-12-28T22:00:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241228-1900").tap do |shift_| - shift_.stage = stage_coordinator - shift_.title = "Coordinator Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-28T18:15:00+01:00" - shift_.ends_at = "2024-12-28T22:00:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241229-1900").tap do |shift_| - shift_.stage = stage_standby - shift_.title = "Standby Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-29T19:00:00+01:00" - shift_.ends_at = "2024-12-29T22:00:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241229-1900").tap do |shift_| - shift_.stage = stage_coordinator - shift_.title = "Coordinator Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-29T18:15:00+01:00" - shift_.ends_at = "2024-12-29T22:00:00+01:00" - shift_.save! -end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241227-2200").tap do |shift_| - shift_.stage = stage_standby - shift_.title = "Standby Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-27T22:00:00+01:00" - shift_.ends_at = "2024-12-28T01:20:00+01:00" - shift_.save! +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-03-20241227").tap do |shift_| +# shift_.stage = stage_service +# shift_.title = "General Angel Meeting" +# shift_.language = "de" +# shift_.description = "Time and place are placeholders, please reconfirm before the start of the shift!" +# shift_.url = "https://engel.events.ccc.de/" +# shift_.starts_at = "2024-12-27T18:00:00+01:00" +# shift_.ends_at = "2024-12-27T19:00:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-03-20241228").tap do |shift_| +# shift_.stage = stage_service +# shift_.title = "General Angel Meeting" +# shift_.language = "de" +# shift_.description = "Time and place are placeholders, please reconfirm before the start of the shift!" +# shift_.url = "https://engel.events.ccc.de/" +# shift_.starts_at = "2024-12-28T18:00:00+01:00" +# shift_.ends_at = "2024-12-28T19:00:00+01:00" +# shift_.save! +# end +# Session.find_or_create_by(conference: orga, ref_id: "c3-4242-03-20241229").tap do |shift_| +# shift_.stage = stage_service +# shift_.title = "General Angel Meeting" +# shift_.language = "de" +# shift_.description = "Time and place are placeholders, please reconfirm before the start of the shift!" +# shift_.url = "https://engel.events.ccc.de/" +# shift_.starts_at = "2024-12-29T18:00:00+01:00" +# shift_.ends_at = "2024-12-29T19:00:00+01:00" +# shift_.save! +# end + + +# if Rails.env.development? +# %w[coverage fog comedy adjust forge fail vigorous promise chemistry conception meat storage certain warm develop civilian cousin injection hammer health appetite conventional good snake grant suspect atmosphere linen wrong deal calf sea management silence watch nuance loan quit convert failure bracket slice sweat treaty plot still chimpanzee assume functional marsh dream mail state dorm kid formation secular agile beach guide salesperson merit goalkeeper incongruous cart pig joystick regulation apparatus myth patent glue behead flu departure spectrum parking indication delay hesitate viable lay treat cooperative sensation auction sphere stain tap pass].each do |username| +# User.find_or_create_by(name: username) do |u| +# u.email = "c3lingo+#{username}@x.moeffju.net" +# u.invitation_token = "gargamel" +# u.save! +# end +# end +# end + +if token = fetch_credential("telegram_bot_token") + NotificationChannel.find_or_create_by(name: "telegram_group_chat").tap do |c| + c.data = { token: token } + c.save! + end end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241227-2200").tap do |shift_| - shift_.stage = stage_coordinator - shift_.title = "Coordinator Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-27T22:00:00+01:00" - shift_.ends_at = "2024-12-28T02:40:00+01:00" - shift_.save! + +# Create RBAC roles and permissions +shift_coordinator_role = Role.find_or_create_by(name: 'shift_coordinator') do |role| + role.description = 'Can manage session assignments and scheduling' end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241228-2200").tap do |shift_| - shift_.stage = stage_standby - shift_.title = "Standby Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-28T22:00:00+01:00" - shift_.ends_at = "2024-12-29T01:20:00+01:00" - shift_.save! + +events_admin_role = Role.find_or_create_by(name: 'events_admin') do |role| + role.description = 'Can manage conferences and all sub-resources' end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241228-2200").tap do |shift_| - shift_.stage = stage_coordinator - shift_.title = "Coordinator Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-28T22:00:00+01:00" - shift_.ends_at = "2024-12-29T02:25:00+01:00" - shift_.save! + +admin_role = Role.find_or_create_by(name: 'admin') do |role| + role.description = 'Can manage users and assign roles' end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-01-20241229-2200").tap do |shift_| - shift_.stage = stage_standby - shift_.title = "Standby Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-29T22:00:00+01:00" - shift_.ends_at = "2024-12-30T01:00:00+01:00" - shift_.save! + +# Create permissions +manage_assignments = Permission.find_or_create_by(name: 'manage_assignments') do |perm| + perm.description = 'Can create and delete assignments' end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-02-20241229-2200").tap do |shift_| - shift_.stage = stage_coordinator - shift_.title = "Coordinator Shift" - shift_.language = "en" - shift_.url = "https://rescheduled.c3lingo.org/" - shift_.starts_at = "2024-12-29T22:00:00+01:00" - shift_.ends_at = "2024-12-30T02:15:00+01:00" - shift_.save! + +manage_conferences = Permission.find_or_create_by(name: 'manage_conferences') do |perm| + perm.description = 'Can create, edit, and delete conferences' end +manage_sessions = Permission.find_or_create_by(name: 'manage_sessions') do |perm| + perm.description = 'Can create, edit, and delete sessions' +end +manage_speakers = Permission.find_or_create_by(name: 'manage_speakers') do |perm| + perm.description = 'Can create, edit, and delete speakers' +end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-03-20241227").tap do |shift_| - shift_.stage = stage_service - shift_.title = "General Angel Meeting" - shift_.language = "de" - shift_.description = "Time and place are placeholders, please reconfirm before the start of the shift!" - shift_.url = "https://engel.events.ccc.de/" - shift_.starts_at = "2024-12-27T18:00:00+01:00" - shift_.ends_at = "2024-12-27T19:00:00+01:00" - shift_.save! +manage_stages = Permission.find_or_create_by(name: 'manage_stages') do |perm| + perm.description = 'Can create, edit, and delete stages' end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-03-20241228").tap do |shift_| - shift_.stage = stage_service - shift_.title = "General Angel Meeting" - shift_.language = "de" - shift_.description = "Time and place are placeholders, please reconfirm before the start of the shift!" - shift_.url = "https://engel.events.ccc.de/" - shift_.starts_at = "2024-12-28T18:00:00+01:00" - shift_.ends_at = "2024-12-28T19:00:00+01:00" - shift_.save! + +manage_users = Permission.find_or_create_by(name: 'manage_users') do |perm| + perm.description = 'Can create, edit, and delete users' end -Session.find_or_create_by(conference: orga, ref_id: "c3-4242-03-20241229").tap do |shift_| - shift_.stage = stage_service - shift_.title = "General Angel Meeting" - shift_.language = "de" - shift_.description = "Time and place are placeholders, please reconfirm before the start of the shift!" - shift_.url = "https://engel.events.ccc.de/" - shift_.starts_at = "2024-12-29T18:00:00+01:00" - shift_.ends_at = "2024-12-29T19:00:00+01:00" - shift_.save! + +assign_roles = Permission.find_or_create_by(name: 'assign_roles') do |perm| + perm.description = 'Can assign roles to users' end +# Associate permissions with roles (using find_or_create to avoid duplicates) +RolePermission.find_or_create_by(role: shift_coordinator_role, permission: manage_assignments) -if Rails.env.development? - %w[coverage fog comedy adjust forge fail vigorous promise chemistry conception meat storage certain warm develop civilian cousin injection hammer health appetite conventional good snake grant suspect atmosphere linen wrong deal calf sea management silence watch nuance loan quit convert failure bracket slice sweat treaty plot still chimpanzee assume functional marsh dream mail state dorm kid formation secular agile beach guide salesperson merit goalkeeper incongruous cart pig joystick regulation apparatus myth patent glue behead flu departure spectrum parking indication delay hesitate viable lay treat cooperative sensation auction sphere stain tap pass].each do |username| - User.find_or_create_by(name: username) do |u| - u.email = "c3lingo+#{username}@x.moeffju.net" - u.invitation_token = "gargamel" - u.save! - end - end -end +RolePermission.find_or_create_by(role: events_admin_role, permission: manage_conferences) +RolePermission.find_or_create_by(role: events_admin_role, permission: manage_sessions) +RolePermission.find_or_create_by(role: events_admin_role, permission: manage_speakers) +RolePermission.find_or_create_by(role: events_admin_role, permission: manage_stages) -if token = fetch_credential("telegram_bot_token") - NotificationChannel.find_or_create_by(name: "telegram_group_chat").tap do |c| - c.data = { token: token } - c.save! - end -end +RolePermission.find_or_create_by(role: admin_role, permission: manage_users) +RolePermission.find_or_create_by(role: admin_role, permission: assign_roles) +RolePermission.find_or_create_by(role: admin_role, permission: manage_assignments) +RolePermission.find_or_create_by(role: admin_role, permission: manage_conferences) +RolePermission.find_or_create_by(role: admin_role, permission: manage_sessions) +RolePermission.find_or_create_by(role: admin_role, permission: manage_speakers) +RolePermission.find_or_create_by(role: admin_role, permission: manage_stages) diff --git a/lib/tasks/admin.rake b/lib/tasks/admin.rake index 290562f1482ddbfa382128e7674a3d5f6a2dfeb5..36c0b0796b8dfa7247da521c0ac993d8388f7f97 100644 --- a/lib/tasks/admin.rake +++ b/lib/tasks/admin.rake @@ -16,15 +16,15 @@ namespace :admin do exit 1 end - # Get the shift_coordinator role - admin_role = Role.find_by(name: "shift_coordinator") + # Get the admin role + admin_role = Role.find_by(name: "admin") if admin_role.nil? - puts "Error: 'shift_coordinator' role does not exist." + puts "Error: 'admin' role does not exist." exit 1 end - if user.has_role?("shift_coordinator") + if user.has_role?("admin") puts "User '#{user.name}' already has the admin role." else user.roles << admin_role @@ -32,26 +32,74 @@ namespace :admin do end end - desc "List all admin users" - task list: :environment do - admin_role = Role.find_by(name: "shift_coordinator") + desc "Assign shift coordinator role to a user by email" + task :make_shift_coordinator, [ :email ] => :environment do |t, args| + email = args[:email] - if admin_role.nil? - puts "Error: 'shift_coordinator' role does not exist." + if email.blank? + puts "Error: Email is required." + puts "Usage: rake admin:make_shift_coordinator[user@example.com]" + exit 1 + end + + user = User.find_by(email: email) + + if user.nil? + puts "Error: User with email '#{email}' not found." exit 1 end - admins = admin_role.users + # Get the shift_coordinator role + shift_coordinator_role = Role.find_by(name: "shift_coordinator") + + if shift_coordinator_role.nil? + puts "Error: 'shift_coordinator' role does not exist." + exit 1 + end - if admins.empty? - puts "No users with admin rights found." + if user.has_role?("shift_coordinator") + puts "User '#{user.name}' already has the shift coordinator role." else - puts "Users with admin rights:" - puts "-----------------------" - admins.each do |admin| - puts "#{admin.name} (#{admin.email || 'No email'})" + user.roles << shift_coordinator_role + puts "Successfully assigned shift coordinator role to '#{user.name}' (#{user.email})." + end + end + + desc "List all users with special roles" + task list: :environment do + admin_role = Role.find_by(name: "admin") + shift_coordinator_role = Role.find_by(name: "shift_coordinator") + events_admin_role = Role.find_by(name: "events_admin") + + puts "Users with special roles:" + puts "-----------------------" + + if admin_role && admin_role.users.any? + puts "\nAdmins:" + admin_role.users.each do |admin| + puts " #{admin.name} (#{admin.email || 'No email'})" + end + end + + if shift_coordinator_role && shift_coordinator_role.users.any? + puts "\nShift Coordinators:" + shift_coordinator_role.users.each do |sc| + puts " #{sc.name} (#{sc.email || 'No email'})" + end + end + + if events_admin_role && events_admin_role.users.any? + puts "\nEvents Admins:" + events_admin_role.users.each do |ea| + puts " #{ea.name} (#{ea.email || 'No email'})" end end + + if (admin_role.nil? || admin_role.users.empty?) && + (shift_coordinator_role.nil? || shift_coordinator_role.users.empty?) && + (events_admin_role.nil? || events_admin_role.users.empty?) + puts "No users with special roles found." + end end desc "Remove admin role from a user by email" @@ -71,19 +119,52 @@ namespace :admin do exit 1 end - # Get the shift_coordinator role - admin_role = Role.find_by(name: "shift_coordinator") + # Get the admin role + admin_role = Role.find_by(name: "admin") if admin_role.nil? - puts "Error: 'shift_coordinator' role does not exist." + puts "Error: 'admin' role does not exist." exit 1 end - if !user.has_role?("shift_coordinator") + if !user.has_role?("admin") puts "User '#{user.name}' does not have the admin role." else user.roles.delete(admin_role) puts "Successfully removed admin role from '#{user.name}' (#{user.email})." end end + + desc "Remove shift coordinator role from a user by email" + task :remove_shift_coordinator, [ :email ] => :environment do |t, args| + email = args[:email] + + if email.blank? + puts "Error: Email is required." + puts "Usage: rake admin:remove_shift_coordinator[user@example.com]" + exit 1 + end + + user = User.find_by(email: email) + + if user.nil? + puts "Error: User with email '#{email}' not found." + exit 1 + end + + # Get the shift_coordinator role + shift_coordinator_role = Role.find_by(name: "shift_coordinator") + + if shift_coordinator_role.nil? + puts "Error: 'shift_coordinator' role does not exist." + exit 1 + end + + if !user.has_role?("shift_coordinator") + puts "User '#{user.name}' does not have the shift coordinator role." + else + user.roles.delete(shift_coordinator_role) + puts "Successfully removed shift coordinator role from '#{user.name}' (#{user.email})." + end + end end