Skip to content
Snippets Groups Projects

Compare revisions

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

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • c3lingo/rescheduled
1 result
Select Git revision
Show changes
Showing
with 975 additions and 578 deletions
<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>
<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>
<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>
<h5 class="text-base mt-2 <%= Time.parse(date).end_of_day < now ? "past" : "future" %>"><%= date %></h5>
<ol class="list-inside">
<% assignments_on_date.each do |assignment| %>
<li class="<%= assignment.session.starts_at < now ? "past" : "future" %> indent-[-1rem] ml-4">
<span class="tabular-nums"><%= assignment.session.starts_at.strftime('%H:%M') %> &ndash; <%= assignment.session.ends_at.strftime('%H:%M') %></span>:
<%= render partial: 'shared/session_filedrop', locals: { session: assignment.session } %><%= link_to assignment.session.title, conference_session_path(assignment.session.conference, assignment.session) %><%= render partial: 'shared/session_engelsystem', locals: { session: assignment.session } %> @ <%= assignment.session.stage.name %>
<small><% assignment.session.assignments.map(&:user).each do |other_user| %>
<%= render partial: 'application/user_avatar', locals: { user: other_user } %>
<% end %></small>
</li>
<% end %>
</ol>
<div class="mb-6 <%= Time.parse(date).end_of_day < now ? "past" : "future" %>">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-3"><%= date %></h3>
<ul class="space-y-3">
<% assignments_on_date.each do |assignment| %>
<li class="<%= assignment.session.starts_at < now ? "past" : "future" %> pl-4 border-l-4 border-gray-200 dark:border-gray-700">
<div class="flex flex-col md:flex-row md:items-center gap-2">
<span class="tabular-nums font-medium text-gray-900 dark:text-gray-100 min-w-28">
<%= assignment.session.starts_at.strftime('%H:%M') %> &ndash; <%= assignment.session.ends_at.strftime('%H:%M') %>
</span>
<div class="flex-1">
<%= render partial: 'shared/session_filedrop', locals: { session: assignment.session } %>
<%= link_to assignment.session.title, conference_session_path(assignment.session.conference, assignment.session), class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" %>
<%= render partial: 'shared/session_engelsystem', locals: { session: assignment.session } %>
<span class="text-gray-500 dark:text-gray-400 ml-1">@ <%= assignment.session.stage.name %></span>
</div>
<div class="flex items-center space-x-1">
<% assignment.session.assignments.map(&:user).each do |other_user| %>
<%= render partial: 'application/user_avatar', locals: { user: other_user } %>
<% end %>
</div>
</div>
</li>
<% end %>
</ul>
</div>
<div class="container mx-auto px-4 py-8">
<div class="max-w-full">
<h1 class="text-xl my-4 dark:text-red-500">
Assignments for
<%= link_to @user.name, user_assignments_path(@user) %>
<span class="text-base ml-2 mb-2 inline p-2 border bg-slate-50 hover:bg-slate-100 border-slate-200 hover:border-slate-200 shadow font-normal rounded-md"><%= link_to user_assignments_path(@user, format: 'ics') do %><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 24" fill="currentColor" aria-hidden="true" class="size-6 inline-block stroke-slate-400 fill-slate-400"><path fill-rule="evenodd" d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z" clip-rule="evenodd"></path></svg> iCal<% end %></span>
</h1>
<% now = Time.now %>
<div>
<h2 class="text-lg my-4">List View</h2>
<% @user.assignments.includes(:session, session: :conference).order('sessions.starts_at').group_by { |a| a.session.starts_at.strftime('%Y-%m-%d') }.each do |date, assignments_on_date| %>
<%= render partial: 'listview_date', locals: { assignments_on_date:, date:, now: } %>
<div class="flex items-center mb-6">
<h1 class="text-2xl font-bold dark:text-gray-200">
Assignments for
<%= link_to @user.name, user_assignments_path(@user), class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" %>
</h1>
<%= link_to user_assignments_path(@user, format: 'ics'), class: "btn btn-info-light ml-4" do %>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 24" fill="currentColor" aria-hidden="true" class="size-5 inline-block mr-1"><path fill-rule="evenodd" d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z" clip-rule="evenodd"></path></svg> iCal
<% end %>
</div>
<% now = Time.now %>
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-4">List View</h2>
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<% @user.assignments.includes(:session, session: :conference).order('sessions.starts_at').group_by { |a| a.session.starts_at.strftime('%Y-%m-%d') }.each do |date, assignments_on_date| %>
<%= render partial: 'listview_date', locals: { assignments_on_date:, date:, now: } %>
<% end %>
</div>
</div>
<div class="overflow-x-auto max-w-full">
<h2 class="text-lg my-4 sticky left-0">Table View</h2>
<table class="border *:border w-full">
<thead>
<tr class="*:font-bold *:border">
<th>Date</th>
<th>Starts</th>
<th>Ends</th>
<th>Stage</th>
<th>Session</th>
<th>Collaborators</th>
</tr>
</thead>
<tbody>
<% @user.assignments.includes(:session, session: :conference).order('sessions.starts_at').each do |assignment| %>
<tr class="*:border *:p-1 <%= assignment.session.ends_at < Time.now ? "past" : "future" %>">
<td><%= assignment.session.starts_at.strftime('%Y-%m-%d') %></td>
<td><%= assignment.session.starts_at.strftime('%H:%M') %></td>
<td><%= assignment.session.ends_at.strftime('%H:%M') %></td>
<td><%= assignment.session.stage.name %></td>
<td><%= render partial: 'shared/session_filedrop', locals: { session: assignment.session } %><%= link_to assignment.session.title, assignment.session.url, target: "_blank" %><%= render partial: 'shared/session_engelsystem', locals: { session: assignment.session } %></td>
<td><% assignment.session.assignments.map(&:user).each do |other_user| %><%= render partial: 'application/user_avatar', locals: { user: other_user } %><% end %></td>
</tr>
<% end %>
</table>
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-4 sticky left-0">Table View</h2>
<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">Date</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">Starts</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">Ends</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">Stage</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">Session</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">Collaborators</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
<% @user.assignments.includes(:session, session: :conference).order('sessions.starts_at').each do |assignment| %>
<tr class="<%= assignment.session.ends_at < Time.now ? "past" : "future" %>">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= assignment.session.starts_at.strftime('%Y-%m-%d') %></td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= assignment.session.starts_at.strftime('%H:%M') %></td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= assignment.session.ends_at.strftime('%H:%M') %></td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= assignment.session.stage.name %></td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-200">
<%= render partial: 'shared/session_filedrop', locals: { session: assignment.session } %>
<%= link_to assignment.session.title, assignment.session.url, target: "_blank", class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" %>
<%= render partial: 'shared/session_engelsystem', locals: { session: assignment.session } %>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
<% assignment.session.assignments.map(&:user).each do |other_user| %>
<%= render partial: 'application/user_avatar', locals: { user: other_user } %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
</div>
......@@ -13,12 +13,12 @@ current_time = Time.zone.now.in_time_zone(@conference.time_zone)
<%= render partial: 'assignments/filteredlist_option', locals: { user: } %>
<% end %>
</template>
<div class="container mx-auto px-4 py-8">
<div class="container mx-auto px-4 py-8 text-black dark:text-white">
<div class="mb-4">
<a href="#now" onclick="document.querySelector('#now')?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }); return false" class="underline text-blue-500">Jump to current time</a>
</div>
<h1 class="text-2xl font-bold my-2 dark:text-red-500"><%= @conference.name %></h1>
<p class="text-xs mb-6">
<h1 class="text-2xl font-bold my-2 text-black dark:text-white"><%= @conference.name %></h1>
<p class="text-xs mb-6 text-black dark:text-white">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="inline-block size-4 stroke-slate-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
......
<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>
<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>
<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>
<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>
<!DOCTYPE html>
<html class="<%= "dark" if current_user&.darkmode == "dark"%>">
<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>
<!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,30 +11,40 @@
<%= 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">
<div class="flex items-center space-x-4">
<h1 class="text-xl font-bold text-gray-900 dark:text-white">
<%= link_to "re:scheduled", root_path %>
</h1>
<%= 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' %>
</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>
......
<header class="bg-gray-800 dark:bg-stone-200 text-white dark:text-black shadow">
<div class="container mx-auto px-4 py-4 flex justify-between items-center">
<div class="flex items-center space-x-4">
<h1 class="text-xl font-bold">
<%= link_to "re:scheduled admin", admin_root_path %>
</h1>
<%= link_to "Dashboard", admin_dashboard_path, class: "text-gray-300 dark:text-gray-600 hover:text-white dark:hover:text-black" %>
<%= link_to "Users", admin_users_path, class: "text-gray-300 dark:text-gray-600 hover:text-white dark:hover:text-black" %>
<%= link_to "Roles", admin_roles_path, class: "text-gray-300 dark:text-gray-600 hover:text-white dark:hover:text-black" %>
<%= link_to "Conferences", admin_conferences_path, class: "text-gray-300 dark:text-gray-600 hover:text-white dark:hover:text-black" %>
</div>
<nav class="flex items-center space-x-4">
<%= render partial: "application/user_avatar", locals: { user: current_user } %>
<%= link_to "Back to Site", root_path, class: "ml-4 text-gray-300 dark:text-gray-600 hover:text-white dark:hover:text-black" %>
</nav>
</div>
</header>
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"
......
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')
......
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
......@@ -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
......
This diff is collapsed.
......@@ -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