diff --git a/app/assets/images/admin/check_in.svg b/app/assets/images/admin/check_in.svg new file mode 100644 index 0000000..392c447 --- /dev/null +++ b/app/assets/images/admin/check_in.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/stylesheets/_admin.scss b/app/assets/stylesheets/_admin.scss index b9980e0..3c441dc 100644 --- a/app/assets/stylesheets/_admin.scss +++ b/app/assets/stylesheets/_admin.scss @@ -812,6 +812,20 @@ nav.sub-menu { } } +@mixin hover-info { + display: none; + position: absolute; + right: 100%; + top: 0; + background-color: $white; + border: 0.1em solid #CCC; + padding: 0.25em 0.75em; + margin: 0; + list-style-type: square; + @include default-box-shadow(top, 2); + z-index: 10; +} + @include keyframes(unhappy) { from { @include _(transform, rotate(15deg)); @@ -872,6 +886,66 @@ nav.sub-menu { vertical-align: top; } + > th { + min-width: 15em; + } + + .host-notes { + @include font-family(primary); + font-size: 0.85em; + border: 0.1em solid $light-gray; + background-color: $white; + padding: 0.5em; + margin: 1em 0; + + > p:first-child { + margin-top: 0; + } + + > p:last-child { + margin-bottom: 0; + } + } + + .guest-notes { + position: relative; + float: left; + margin-top: -0.2em; + margin-right: 0.25em; + cursor: pointer; + + @include after { + content: '\1F4C4'; + width: 2em; + height: 2em; + } + + .notes { + @include hover-info; + top: auto; + bottom: 100%; + right: auto; + min-width: 25em; + + p { + font-size: 1.125em; + + &:first-child { + margin-top: 0; + } + &:last-child { + margin-bottom: 0; + } + } + } + + &:hover { + .notes { + display: block; + } + } + } + .address { margin-top: 1em; text-align: right; @@ -937,17 +1011,7 @@ nav.sub-menu { } ul { - display: none; - position: absolute; - right: 100%; - top: 0; - background-color: $white; - border: 0.1em solid #CCC; - padding: 0.25em 0.75em; - margin: 0; - list-style-type: square; - @include default-box-shadow(top, 2); - z-index: 10; + @include hover-info; } li { @@ -1719,6 +1783,113 @@ html[data-ontop] { } } +.search-message { + display: none; + border: 0.05em solid $gray; + text-align: center; + padding: 1em; + margin: 1em; + border-radius: 0.2em; + box-shadow: 0 0 1em -0.5em; +} + +#search-results { + display: none; + width: 100%; + margin-left: 0; + + a { + color: inherit; + + @include after { + display: none; + } + } + + .name { + font-size: 1.5em; + min-width: 10em; + } + + .registration { + &:hover, &:focus { + background-color: lighten($colour-1, 33%); + cursor: pointer; + + th { + background-color: lighten($colour-1, 25%); + } + } + } +} + +#search-form { + #no-search { + display: block; + } + + #new-user { + display: none; + } + + &[data-status="no-results"] { + #no-search, #search-results, #new-user { + display: none; + } + + #no-results { + display: block; + } + } + + &[data-status="success"] { + #no-search, #no-results, #new-user { + display: none; + } + + #search-results { + display: block; + } + } + + &[data-status="new-user"] { + #no-search, #no-results, #search-results { + display: none; + } + + #new-user { + display: block; + } + } +} + +#check-in { + .currency { + font-size: 1.5em; + margin-top: 0.5em; + margin-right: 0.5em; + + ~ .select-field { + margin-top: 0.25em; + margin-right: 0.5em; + } + } + + .input-field { + margin-bottom: 0; + } +} + +.back-to-start { + @include before { + content: '◀'; + font-size: 0.65em; + margin-right: 0.333em; + display: inline-block; + vertical-align: bottom; + } +} + @include breakpoint(medium) { nav.sub-nav { float: right; diff --git a/app/assets/stylesheets/_application.scss b/app/assets/stylesheets/_application.scss index a958a9a..3ab5328 100644 --- a/app/assets/stylesheets/_application.scss +++ b/app/assets/stylesheets/_application.scss @@ -384,6 +384,10 @@ table, .table { } } +.no-wrap { + white-space: nowrap; +} + body.modal-open { overflow: hidden; } @@ -2432,8 +2436,8 @@ a.logo { } .register-link { - font-size: 1.25em; - margin: 0.5em; + font-size: 0.75em; + margin: 0.5em 0 0; } } @@ -3632,8 +3636,6 @@ body.policy .policy-agreement ul { } .conference-banner { - @include _-(display, flex); - @include _(flex-wrap, wrap); margin: 0 auto; width: 100%; diff --git a/app/controllers/conference_administration_controller.rb b/app/controllers/conference_administration_controller.rb index 1354ef8..ac0d8a3 100644 --- a/app/controllers/conference_administration_controller.rb +++ b/app/controllers/conference_administration_controller.rb @@ -84,6 +84,30 @@ class ConferenceAdministrationController < ApplicationController end end + def check_in + set_conference + return do_403 unless @this_conference.host? current_user + + @page_title_vars = { title: @this_conference.title } + @admin_step = :check_in + @admin_group = view_context.get_administration_group(@admin_step) + + if params[:id] =~ /^\S+@\S+\.\S{2,}$/ + @user = User.new(email: params[:id]) + elsif params[:id] =~ /^\d+$/ + @user = User.find(params[:id].to_i) + else + return do_404 + end + + @registration = @this_conference.registration_for(@user) || ConferenceRegistration.new(conference: @this_conference) + @registration.data ||= {} + + @user_name = @user.firstname || 'this person' + @user_name_proper = @user.firstname || 'This person' + @user_name_for_title = @user.firstname || "#{@user.email}" + end + rescue_from ActiveRecord::PremissionDenied do |exception| do_403 end @@ -199,7 +223,7 @@ class ConferenceAdministrationController < ApplicationController name: org.name, street_address: address.present? ? address.street : nil, city: address.present? ? address.city : nil, - subregion: address.present? ? I18n.t("geography.subregions.#{address.country}.#{address.territory}") : nil, + subregion: address.present? ? I18n.t("geography.subregions.#{address.country}.#{address.territory}", resolve: false) : nil, country: address.present? ? I18n.t("geography.countries.#{address.country}") : nil, postal_code: address.present? ? address.postal_code : nil, email: org.email_address, @@ -299,6 +323,46 @@ class ConferenceAdministrationController < ApplicationController end end + def administrate_check_in + sort_weight = { + checked_in: 5, + registered: 4, + incomplete: 3, + cancelled: 2, + unregistered: 1 + } + + @registration_data = [] + User.all.each do |user| + if user.email.present? + new_data = { + user_id: user.id, + email: user.email, + name: user.firstname + } + + organization = user.organizations.first + new_data[:organization] = organization.present? ? organization.name : '' + + registration = @this_conference.registration_for(user) + if registration.present? && registration.city_id.present? + new_data[:location] = registration.city.to_s + status = registration.status + else + new_data[:location] = user.last_location.to_s + status = :unregistered + end + + new_data[:status] = I18n.t("articles.conference_registration.terms.registration_status.#{status}") + new_data[:sort_weight] = sort_weight[status] + + @registration_data << new_data + end + end + + @registration_data.sort! { |a, b| b[:sort_weight] <=> a[:sort_weight] } + end + def administrate_stats if @this_conference.start_date.blank? || @this_conference.end_date.blank? @warning_message = :no_date_warning @@ -476,6 +540,7 @@ class ConferenceAdministrationController < ApplicationController @excel_data = { columns: [ :name, + :pronoun, :email, :date, :status, @@ -524,6 +589,7 @@ class ConferenceAdministrationController < ApplicationController }, keys: { name: 'forms.labels.generic.name', + pronoun: 'forms.labels.generic.pronoun', email: 'forms.labels.generic.email', status: 'forms.labels.generic.registration_status', is_attending: 'articles.conference_registration.terms.is_attending', @@ -590,6 +656,7 @@ class ConferenceAdministrationController < ApplicationController data = { id: r.id, name: user.firstname || '', + pronoun: user.pronoun || '', email: user.email || '', status: I18n.t("articles.conference_registration.terms.registration_status.#{view_context.registration_status(r)}"), is_attending: I18n.t("articles.conference_registration.questions.bike.#{r.is_attending == 'n' ? 'no' : 'yes'}"), @@ -1101,6 +1168,9 @@ class ConferenceAdministrationController < ApplicationController # delete deprecated values registration.allergies = nil registration.other = nil + when :org_non_member_interest + registration.data ||= {} + registration.data['non_member_interest'] = value when :registration_fees_paid registration.data ||= {} registration.data['payment_amount'] = value.to_f @@ -1116,8 +1186,8 @@ class ConferenceAdministrationController < ApplicationController registration.housing_data['companion'] ||= {} registration.housing_data['companion']['email'] = value registration.housing_data['companion']['id'] = User.find_user(value).id - when :preferred_language - registration.user.locale = value + when :preferred_language, :pronoun + registration.user.send("#{key}=", value) user_changed = true when :is_subscribed registration.user.is_subscribed = (value != "false") @@ -1180,6 +1250,67 @@ class ConferenceAdministrationController < ApplicationController return nil end + def admin_update_check_in + unless params[:button] == 'cancel' + user_id = params[:user_id] + + if params[:user_id].present? + user_id = user_id.to_i + else + user_id = User.get(params[:email]).id + end + + registration = ConferenceRegistration.where( + user_id: user_id, + conference_id: @this_conference.id + ).limit(1).first || + ConferenceRegistration.new( + conference_id: @this_conference.id, + user_id: user_id + ) + + registration.data ||= {} + registration.data['checked_in'] ||= DateTime.now + + if params[:payment] + amount = params[:payment].to_f + if amount > 0 + registration.registration_fees_paid ||= 0 + registration.registration_fees_paid += amount + registration.data['payment_amount'] = amount + registration.data['payment_currency'] ||= params[:currency] + end + end + + user = nil + if params[:name].present? + user ||= registration.user + user.firstname ||= params[:name] + end + + if params[:pronoun].present? + user ||= registration.user + user.pronoun ||= params[:pronoun] + end + + if params[:location].present? + unless registration.city_id.present? + city = City.search(params[:location]) + registration.city_id = city.id if city.present? + end + end + + user.save if user.present? + + registration.bike = params[:bike] + registration.data['programme'] = params[:programme] + + registration.save + end + + return false + end + def admin_update_housing # modify the guest data if params[:button] == 'get-guest-list' diff --git a/app/controllers/conferences_controller.rb b/app/controllers/conferences_controller.rb index efe822d..ff88956 100644 --- a/app/controllers/conferences_controller.rb +++ b/app/controllers/conferences_controller.rb @@ -104,297 +104,6 @@ class ConferencesController < ApplicationController end end - def old_register - set_conference - - @register_template = nil - - if logged_in? - set_or_create_conference_registration - - @name = current_user.firstname - # we should phase out last names - @name += " #{current_user.lastname}" if current_user.lastname - - @name ||= current_user.username - - @is_host = @this_conference.host? current_user - else - @register_template = :confirm_email - end - - steps = nil - return do_404 unless registration_steps.present? - - # @register_template = :administration if params[:admin_step].present? - - @errors = {} - @warnings = [] - form_step = params[:button] ? params[:button].to_sym : nil - - # process any data that was passed to us - if form_step - if form_step.to_s =~ /^prev_(.+)$/ - steps = registration_steps - @register_template = steps[steps.find_index($1.to_sym) - 1] - elsif form_step == :paypal_confirm - if @registration.present? && @registration.payment_confirmation_token == params[:confirmation_token] - if Rails.env.test? - @amount = params[:amount].to_f - info = YAML.load(@registration.payment_info) - info[:amount] = @amount - @registration.payment_info = info.to_yaml - else - @amount = PayPal!.details(params[:token]).amount.total - @registration.payment_info = { - payer_id: params[:PayerID], - token: params[:token], - amount: @amount - }.to_yaml - end - - @amount = (@amount * 100).to_i.to_s.gsub(/^(.*)(\d\d)$/, '\1.\2') - - @registration.save! - end - - @page_title = 'articles.conference_registration.headings.Payment' - @register_template = :paypal_confirm - elsif form_step == :paypal_confirmed - info = YAML.load(@registration.payment_info) - @amount = nil - status = nil - if Rails.env.test? - status = info[:status] - @amount = info[:amount] - else - paypal = PayPal!.checkout!(info[:token], info[:payer_id], PayPalRequest(info[:amount])) - status = paypal.payment_info.first.payment_status - @amount = paypal.payment_info.first.amount.total - end - if status == 'Completed' - @registration.registration_fees_paid ||= 0 - @registration.registration_fees_paid += @amount - - # don't complete the step unless fees have been paid - if @registration.registration_fees_paid > 0 - @registration.steps_completed << :payment - @registration.steps_completed.uniq! - end - - @registration.save! - else - @errors[:payment] = :incomplete - @register_template = :payment - end - @page_title = 'articles.conference_registration.headings.Payment' - else - - case form_step - when :confirm_email - return confirm_email(params[:email], params[:token], register_path(@this_conference.slug)) - when :contact_info - if params[:name].present? && params[:name].gsub(/[\s\W]/, '').present? - current_user.firstname = params[:name].squish - current_user.lastname = nil - else - @errors[:name] = :empty - end - - if params[:location].present? && params[:location].gsub(/[\s\W]/, '').present? - city = City.search(params[:location]) - - if city.present? - @registration.city_id = city.id - if params[:location].gsub(/[\s,]/, '').downcase != view_context.location(city).gsub(/[\s,]/, '').downcase - @warnings << view_context._('warnings.messages.location_corrected', vars: {original: params[:location], corrected: view_context.location(city)}) - end - else - @errors[:location] = :unknown - end - else - @errors[:location] = :empty - end - - if params[:languages].present? - current_user.languages = params[:languages].keys - else - @errors[:languages] = :empty - end - - current_user.save! unless @errors.present? - when :hosting - @registration.can_provide_housing = params[:can_provide_housing].present? - if params[:not_attending] - @registration.is_attending = 'n' - - if current_user.is_subscribed.nil? - current_user.is_subscribed = false - current_user.save! - end - else - @registration.is_attending = 'y' - end - - @registration.housing_data = { - address: params[:address], - phone: params[:phone], - space: { - bed_space: params[:bed_space], - floor_space: params[:floor_space], - tent_space: params[:tent_space], - }, - considerations: (params[:considerations] || {}).keys, - availability: [ params[:first_day], params[:last_day] ], - notes: params[:notes] - } - when :questions - # create the companion's user account and send a registration link unless they have already registered - generate_confirmation(User.create(email: params[:companion]), register_path(@this_conference.slug)) if params[:companion].present? && User.find_user(params[:companion]).nil? - - @registration.housing = params[:housing] - @registration.arrival = params[:arrival] - @registration.departure = params[:departure] - @registration.housing_data = { - companions: [ params[:companion] ] - } - @registration.bike = params[:bike] - @registration.food = params[:food] - @registration.allergies = params[:allergies] - @registration.other = params[:other] - when :payment - amount = params[:amount].to_f - - if amount > 0 - # we can't really test paypal integration in our tests, so we'll fake it instead - if Rails.env.test? - @registration.payment_confirmation_token = 'token' - @registration.payment_info = {amount: amount}.to_yaml - @registration.save! - redirect_to 'https://www.paypal.com' - else - @registration.payment_confirmation_token = Digest::SHA256.hexdigest(rand(Time.now.to_f * 1000000).to_i.to_s) - @registration.save! - pp = PayPal! - response = pp.setup( - PayPalRequest(amount), - register_paypal_confirm_url(@this_conference.slug, :paypal_confirm, @registration.payment_confirmation_token), - register_paypal_confirm_url(@this_conference.slug, :paypal_cancel, @registration.payment_confirmation_token), - noshipping: true, - version: 204 - ) - redirect_to response.redirect_uri - end - return - end - end - - if @errors.present? - @register_template = form_step - else - unless @registration.nil? - steps = registration_steps - step_index = steps.find_index(form_step) - @register_template = steps[step_index + 1] if step_index.present? - - # have we reached a new level? - unless @registration.steps_completed.include? form_step.to_s - # this step is only completed if a payment has been made - if form_step != :payment || (@registration.registration_fees_paid || 0) > 0 - @registration.steps_completed ||= [] - @registration.steps_completed << form_step.to_s - @registration.steps_completed.uniq! - end - end - - @registration.save! - end - end - end - end - - steps ||= registration_steps - - # make sure we're on a valid step - @register_template ||= (params[:step] || view_context.current_step).to_sym - - if logged_in? && @register_template != :paypal_confirm - # if we're logged in - if !steps.include?(@register_template) - # and we are not viewing a valid step - return redirect_to register_path(@this_conference.slug) - elsif @register_template != view_context.current_step && !registration_complete? && !@registration.steps_completed.include?(@register_template.to_s) - # or the step hasn't been reached, registration is not yet complete, and we're not viewing the latest incomplete step - return redirect_to register_path(@this_conference.slug) - end - # then we'll redirect to the current registration step - end - - # prepare the form - case @register_template - when :questions - # see if someone else has asked to be your companion - if @registration.housing_data.blank? - ConferenceRegistration.where( - conference_id: @this_conference.id, can_provide_housing: [nil, false] - ).where.not(housing_data: nil).each do |r| - @registration.housing_data = { - companions: [ r.user.email ] - } if r.housing_data['companion'].present? && (r.housing_data['companion']['id'] == current_user.id || r.housing_data['companion']['email'] == current_user.email) - end - - @registration.housing_data ||= { } - end - @page_title = 'articles.conference_registration.headings.Registration_Info' - when :payment - @page_title = 'articles.conference_registration.headings.Payment' - when :workshops - @page_title = 'articles.conference_registration.headings.Workshops' - - # initialize our arrays - @my_workshops = Array.new - @requested_workshops = Array.new - @workshops_in_need = Array.new - @workshops = Array.new - - # put wach workshop into the correct array - Workshop.where(conference_id: @this_conference.id).each do |workshop| - if workshop.active_facilitator?(current_user) - @my_workshops << workshop - elsif workshop.requested_collaborator?(current_user) - @requested_workshops << workshop - elsif workshop.needs_facilitators - @workshops_in_need << workshop - else - @workshops << workshop - end - end - - # sort the arrays by name - @my_workshops.sort! { |a, b| a.title.downcase <=> b.title.downcase } - @requested_workshops.sort! { |a, b| a.title.downcase <=> b.title.downcase } - @workshops_in_need.sort! { |a, b| a.title.downcase <=> b.title.downcase } - @workshops.sort! { |a, b| a.title.downcase <=> b.title.downcase } - when :contact_info - @page_title = 'articles.conference_registration.headings.Contact_Info' - when :hosting - @page_title = 'articles.conference_registration.headings.Hosting' - @hosting_data = @registration.housing_data || {} - @hosting_data['space'] ||= Hash.new - @hosting_data['availability'] ||= Array.new - @hosting_data['considerations'] ||= Array.new - when :policy - @page_title = 'articles.conference_registration.headings.Policy_Agreement' - when :confirm_email - @page_title = "articles.conference_registration.headings.#{@this_conference.registration_status == :open ? '': 'Pre_'}Registration_Details" - @main_title = "articles.conference_registration.headings.#{@this_conference.registration_status == :open ? '': 'Pre_'}Register" - @main_title_vars = { vars: { title: @this_conference.title } } - end - - end - - # helper_method :registration_steps - # helper_method :current_registration_steps helper_method :registration_complete? def registration_steps(conference = nil) diff --git a/app/controllers/workshops_controller.rb b/app/controllers/workshops_controller.rb index 49792ba..fd60519 100644 --- a/app/controllers/workshops_controller.rb +++ b/app/controllers/workshops_controller.rb @@ -2,7 +2,7 @@ class WorkshopsController < ApplicationController def workshops set_conference - set_conference_registration! + set_conference_registration @workshops = Workshop.where(conference_id: @this_conference.id) @my_workshops = @workshops.select { |w| w.active_facilitator?(current_user) } render 'workshops/index' @@ -10,7 +10,7 @@ class WorkshopsController < ApplicationController def view_workshop set_conference - set_conference_registration! + set_conference_registration @workshop = Workshop.find_by_id_and_conference_id(params[:workshop_id], @this_conference.id) return do_404 unless @workshop diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb index 2cb13b2..848354c 100644 --- a/app/helpers/admin_helper.rb +++ b/app/helpers/admin_helper.rb @@ -28,7 +28,8 @@ module AdminHelper :registration_status, :stats, :registrations, - :broadcast + :broadcast, + :check_in ], housing: [ :providers, diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index 91627c0..a488b9e 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -228,7 +228,11 @@ module FormHelper so = select_options select_options = [] so.each do |opt| - select_options << [ I18n.t("forms.options.#{name.to_s}.#{opt.to_s}"), opt] + if opt.is_a?(Array) + select_options << opt + else + select_options << [ I18n.t("forms.options.#{name.to_s}.#{opt.to_s}"), opt] + end end end textfield(name, value, options.merge({type: :select, options: select_options})) @@ -470,11 +474,11 @@ module FormHelper if labels.present? label = labels[i] elsif is_single - label = _(label_key.to_s) + label = options[:translate] == false ? label_key.to_s : _(label_key.to_s) elsif box.is_a?(Integer) label = I18n.t(label_key.to_s)[box] else - label = _("#{label_key.to_s}.#{box}") + label = options[:translate] == false ? box : _("#{label_key.to_s}.#{box}") end boxes_html += label_tag(id, label) diff --git a/app/helpers/table_helper.rb b/app/helpers/table_helper.rb index c355465..2238a6a 100644 --- a/app/helpers/table_helper.rb +++ b/app/helpers/table_helper.rb @@ -367,6 +367,7 @@ module TableHelper class: ['registrations', 'admin-edit'], primary_key: :id, column_names: [ + :pronoun, :registration_fees_paid, :payment_currency, :payment_method, @@ -377,6 +378,7 @@ module TableHelper :arrival, :departure, :group_ride, + :org_non_member_interest, :housing, :bike, :food, diff --git a/app/helpers/widgets_helper.rb b/app/helpers/widgets_helper.rb index a9f4b23..73ad5a2 100644 --- a/app/helpers/widgets_helper.rb +++ b/app/helpers/widgets_helper.rb @@ -133,8 +133,17 @@ module WidgetsHelper status_html = content_tag(:ul, status_html.html_safe) end + name_html = guest[:guest].user.name + + other = (guest[:guest].housing_data || {})['other'] + other.strip! if other.present? + + if other.present? + name_html += content_tag :div, (content_tag :div, paragraph(other), class: 'notes').html_safe, class: 'guest-notes' + end + guest_rows += content_tag :tr, id: "hosted-guest-#{guest_id}" do - (content_tag :td, guest[:guest].user.name) + + (content_tag :td, name_html.html_safe) + (content_tag :td do (guest[:guest].from + (content_tag :a, (_'actions.workshops.Remove'), href: '#', class: 'remove-guest', data: { guest: guest_id })).html_safe diff --git a/app/views/conference_administration/_check_in.html.haml b/app/views/conference_administration/_check_in.html.haml new file mode 100644 index 0000000..581af9f --- /dev/null +++ b/app/views/conference_administration/_check_in.html.haml @@ -0,0 +1,174 @@ +%script#registration-data{type: :json}=@registration_data.to_json.to_s.html_safe += columns(medium: 12) do + = admin_update_form id: 'search-form' do + = searchfield :search, nil, big: true + %table#search-results + %thead + %tr + %th.corner + %th Email + %th Location + %th Organization + %th Status + %tbody + + %p#no-search.search-message Search for a user by name, email, location, or organization + %p#no-results.search-message No matching user was found, enter an email address to regster a new user + #new-user.actions.center=link_to 'Register %{email}', check_in_path(@this_conference.slug, 'new_user').gsub('new_user', '%{url_email}'), class: :button + %template#search-result + %tr.registration{tabindex: 0} + %th.name= link_to '%{name}', check_in_path(@this_conference.slug, 'user_id').gsub('user_id', '%{user_id}') + %td %{email} + %td %{location} + %td %{organization} + %td.no-wrap %{status} +:javascript + var searchTable = null, + searchField = null, + lastSearch = null, + registrationData = null, + newUserMessage = null, + newUserMessageTemplate = null, + searchResultTemplate = null, + searchForm = null, + searchFields = ['email', 'name', 'location', 'oranization']; + + function getRegistrationData() { + return JSON.parse(document.getElementById('registration-data').innerHTML); + } + + function getSearchTable() { + return document.getElementById('search-results').getElementsByTagName('tbody')[0]; + } + + function getSearchResultTemplate() { + return document.getElementById('search-result').innerHTML; + } + + function matchScore(data, terms) { + var score = 0; + for (var i = 0; i < terms.length; i++) { + var keys = Object.keys(data), termPos = -1; + for (var j = 0; j < keys.length; j++) { + var dataItem = data[keys[j]]; + if (typeof(dataItem) === "string" && dataItem.length > 0) { + dataItem = dataItem.toLocaleLowerCase(); + var index = dataItem.indexOf(' ' + terms[i]); + if (index < 0) { + index = dataItem.indexOf(terms[i]); + } else { + index = 0; + } + if (index >= 0 && (termPos < 0 || index < termPos)) { + termPos = index; + } + } + } + if (termPos >= 0) { + score += (termPos > 0 ? 10 : 20); + } else { + return 0; + } + } + return score + data['sort_weight']; + } + + function searchResultHTML(data) { + if (searchResultTemplate === null) { + searchResultTemplate = getSearchResultTemplate(); + } + + var keys = Object.keys(data), html = searchResultTemplate; + for (var i = 0; i < keys.length; i++) { + var value = data[keys[i]]; + if (value === null) { + value = ''; + } + html = html.replace(new RegExp('%\\{' + keys[i] + '\\}', 'ig'), value); + } + + return html; + } + + function filterSearchResults() { + if (searchTable === null) { + searchTable = getSearchTable(); + } + if (searchField === null) { + searchField = document.getElementById('search'); + } + if (searchForm === null) { + searchForm = document.getElementById('search-form'); + } + + var searchTerm = searchField.value.toLocaleLowerCase().trim(); + + if (searchTerm != lastSearch) { + searchForm.classList.add('requesting'); + + var range = document.createRange(); + range.selectNodeContents(searchTable); + range.deleteContents(); + + lastSearch = searchTerm; + var status = null; + + if (searchTerm.length > 0) { + var terms = searchTerm.split(/\s+/); + if (registrationData === null) { + registrationData = getRegistrationData(); + } + + var matches = []; + for (var i = 0; i < registrationData.length; i++) { + var score = matchScore(registrationData[i], terms); + if (score > 0) { + matches.push({ score: score, data: registrationData[i] }); + } + } + + if (matches.length > 0) { + matches.sort(function(a, b) { return b.score - a.score; }); + + var html = ''; + for (var i = 0; i < matches.length; i++) { + html += searchResultHTML(matches[i].data); + } + searchTable.innerHTML = html; + + status = 'success'; + } else if (searchTerm.match(/^\S+@\S+\.\S{2,}$/)) { + status = 'new-user'; + if (newUserMessage === null) { + newUserMessage = document.getElementById('new-user'); + newUserMessageTemplate = newUserMessage.innerHTML; + } + newUserMessage.innerHTML = newUserMessageTemplate.replace(/%\{email\}/g, searchTerm).replace(/%\{url_email\}/g, encodeURIComponent(searchTerm)); + } else { + status = 'no-results'; + } + } else { + status = 'no-search'; + } + + searchForm.setAttribute('data-status', status); + searchForm.classList.remove('requesting'); + } + } + + document.addEventListener('click', function(event) { + if (searchTable === null) { + searchTable = getSearchTable(); + } + var target = event.target; + if (searchTable.contains(target)) { + while (target.tagName !== 'TR') { + target = target.parentElement; + } + var link = target.getElementsByTagName('a')[0]; + window.location.href = link.href; + } + }); + document.addEventListener('keyup', filterSearchResults); + + filterSearchResults(); diff --git a/app/views/conference_administration/_hosts_table.html.haml b/app/views/conference_administration/_hosts_table.html.haml index d20f1a5..21ed137 100644 --- a/app/views/conference_administration/_hosts_table.html.haml +++ b/app/views/conference_administration/_hosts_table.html.haml @@ -16,5 +16,7 @@ %th .name=registration.user.name .address=registration.housing_data['address'] + - if registration.housing_data['notes'].present? + .host-notes=paragraph(registration.housing_data['notes']) %td.inner-table{colspan: 2}=host_guests_table(registration) - first_row = false diff --git a/app/views/conference_administration/_housing.html.haml b/app/views/conference_administration/_housing.html.haml index f5564b8..ba6a7e8 100644 --- a/app/views/conference_administration/_housing.html.haml +++ b/app/views/conference_administration/_housing.html.haml @@ -7,4 +7,4 @@ %h3 Select a Guest #table .actions.center - = link_to (_'links.download.Excel'), administration_step_path(@this_conference.slug, @admin_step, :format => :xlsx), class: [:button, :download] + = link_to (_'links.download.Excel'), administration_step_path(@this_conference.slug, @admin_step, format: :xlsx), class: [:button, :download] diff --git a/app/views/conference_administration/administration_step.html.haml b/app/views/conference_administration/administration_step.html.haml index 7b6396a..211975c 100644 --- a/app/views/conference_administration/administration_step.html.haml +++ b/app/views/conference_administration/administration_step.html.haml @@ -13,8 +13,8 @@ = columns(medium: 12) do %nav.sub-nav %ul - %li=link_to (_'articles.admin.headings.back'), administrate_conference_path(@this_conference.slug) - - administration_steps[@admin_group].each do | step | + %li=link_to (_'articles.admin.headings.back'), administrate_conference_path(@this_conference.slug), class: 'back-to-start' + - administration_steps[@admin_group].each do |step| %li - title = (_"articles.admin.#{@admin_group}.headings.#{step}", :t) - if step == @admin_step.to_sym diff --git a/app/views/conference_administration/check_in.html.haml b/app/views/conference_administration/check_in.html.haml new file mode 100644 index 0000000..5390f5b --- /dev/null +++ b/app/views/conference_administration/check_in.html.haml @@ -0,0 +1,94 @@ +- body_class 'banner-bottom' unless @this_conference.poster.present? +- add_stylesheet :admin +- content_for :banner do + = render partial: 'application/header', locals: { page_group: :administration, page_key: 'Administration', image_file: @this_conference.poster_url || 'admin.jpg'} + +%article{id: "admin-#{@admin_step}"} + = row do + = columns(medium: 12) do + - if admin_help_pages[@admin_step.to_sym] + = link_help_dlg("admin_#{admin_help_pages[@admin_step.to_sym]}", class: ['button', 'help-link']) + %h2.floating=_("articles.admin.#{@admin_group}.headings.check_in_user", vars: { name: @user_name_for_title }).html_safe + = row do + = columns(medium: 12) do + %nav.sub-nav + %ul + %li=link_to (_'articles.admin.headings.back'), administrate_conference_path(@this_conference.slug), class: 'back-to-start' + - administration_steps[@admin_group].each do |step| + %li + - title = (_"articles.admin.#{@admin_group}.headings.#{step}", :t) + - if step == @admin_step.to_sym + = title + - else + = link_to title, administration_step_path(@this_conference.slug, step.to_s) + = row do + = admin_update_form do + = columns(medium: 12) do + %p="Please verify with #{@user_name} that the following is correct. If you need to change check in information later, you can check the person in again to overwrite these values." + - if @user.id.present? + = hidden_field_tag :user_id, @user.id + - else + = hidden_field_tag :email, @user.email + = columns(medium: 12) do + %table#check-in{aria: { role: :presentation }} + - unless @user.firstname.present? + %tr + %td + %p="What is their name?" + %td= textfield :name, nil, big: true, label: false, required: true + - if @user.pronoun.nil? + %tr + %td + %p="Does #{@user_name} have a preferred pronoun? If so, enter it here" + %td= textfield :pronoun, nil, label: false + - unless @registration.city.present? + %tr + %td + %p="What city is #{@user_name} based? (Please be specific)" + %td= textfield :location, @user.last_location, label: false + %tr + %td + %p="Did you give #{@user_name} a programme and any other informational materials?" + %td= selectfield :programme, 'yes', [["I gave #{@user_name} a programme", 'yes'], ["I DID NOT give #{@user_name} a programme", 'no']], stretch: true, label: false + %tr + %td + - if @registration.bike.to_s == 'yes' + %p="#{@user_name_proper} said they do need a bike".html_safe + - elsif @registration.bike.to_s == 'no' + %p="#{@user_name_proper} said they do not need a bike".html_safe + - else + %p="Does #{@user_name} need a bike?".html_safe + %td= selectfield :bike, @registration.bike.to_s, [["#{@user_name_proper} is taking a bike", 'yes'], ["#{@user_name_proper} is NOT taking a bike", 'no']], stretch: true, label: false + %tr + %td + %p + - amount = @registration.registration_fees_paid || 0 + - currency = @registration.conference.default_currency + - can_change_currency = true + - if amount > 0 + - currency = @registration.data['payment_currency'] if @registration.data['payment_currency'].present? + ="#{@user_name_proper} has already paid #{number_to_currency amount, unit: '$'} #{currency}, if they decide to make another donation you can add the amount here".html_safe + - amount = 0 + - can_change_currency = false + - else + - amount = @registration.data['payment_amount'] || 0 + - if amount > 0 + ="#{@user_name_proper} has pledged to pay #{number_to_currency amount, unit: '$'} #{@registration.data['payment_currency']}, please confirm and take their payment now".html_safe + - elsif @registration.data['payment_method'].present? + ="#{@user_name_proper} has not pledged to pay for registration. If they would like to pay for registration now, enter their donation amount here".html_safe + - else + ="Please collect registration fees from #{@user_name} if they are willing to donate and enter the amount here".html_safe + %td + .flex-column + .currency $ + = numberfield :payment, amount || 0.0, required: true, step: 0.01, min: 0.0, inline: true, label: false, stretch: true + - if can_change_currency + = selectfield :currency, currency, [:CAD, :USD], inline: true, label: false, inline: true, label: false + - else + .currency + = currency + = hidden_field_tag :currency, currency + = columns(medium: 12) do + .actions.center + = button :check_in + = button :cancel, value: :cancel diff --git a/app/views/conferences/_banner_image.svg.erb b/app/views/conferences/_banner_image.svg.erb deleted file mode 100644 index 89a5852..0000000 --- a/app/views/conferences/_banner_image.svg.erb +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app/views/conferences/_conference.html.haml b/app/views/conferences/_conference.html.haml index 12a762b..ad60f64 100644 --- a/app/views/conferences/_conference.html.haml +++ b/app/views/conferences/_conference.html.haml @@ -10,8 +10,9 @@ - if conference.start_date.present? && conference.end_date.present? .secondary = date_span(conference.start_date.to_date, conference.end_date.to_date) - .register-link - = (link_to (_'forms.actions.generic.register'), register_path(conference.slug), class: [:button, :register]) if links.include?(:register) && conference.can_register? + - if conference.poster.present? && links.include?(:register) && conference.can_register? + .register-link + = (link_to _(is_registered ? 'actions.conference.edit_registration' : 'forms.actions.generic.register'), register_path(conference.slug), class: [:button, :register]) - if conference.poster.present? %figure %img{src: conference.poster.full.url, role: :presentation, alt: (_'images.conference.poster', vars: { conference_title: conference.title })} @@ -20,26 +21,21 @@ = columns(medium: 10, push: {medium: 1}) do %h2=_!conference.title if conference.poster.present? = richtext conference.info - - conference.extended_details.each do |section| - - if sections.include?(section) && conference.copy_data[section][:show] - %h3=(_ conference.copy_data[section][:heading], vars: conference.copy_data[section][:vars]) unless conference.copy_data[section][:heading] == false - = richtext conference.copy_data[section][:value], (conference.copy_data[section][:heading] == false ? 2 : 3) - + .links = (link_to (_(is_registered ? 'actions.conference.edit_registration' : 'forms.actions.generic.register')), register_path(conference.slug), class: [:button, :register]) if links.include?(:register) && conference.can_register? = (link_to (_'articles.workshops.info.read_more'), conference_path(conference.slug), class: :button) if links.include?(:read_more) = (link_to (_'forms.actions.generic.administrate'), administrate_conference_path(conference.slug), class: [:button]) if links.include?(:administrate) = (link_to (_'forms.actions.generic.edit'), edit_conference_path(conference.slug), class: [:button, :subdued]) if links.include?(:edit) - - if conference.registration_status == :open && sections.include?(:workshops) + - conference.extended_details.each do |section| + - if sections.include?(section) && conference.copy_data[section][:show] + %h3{id: section}=(_ conference.copy_data[section][:heading], vars: conference.copy_data[section][:vars]) unless conference.copy_data[section][:heading] == false + = richtext conference.copy_data[section][:value], (conference.copy_data[section][:heading] == false ? 2 : 3) + - if section == :workshop_info + .actions.center= link_to (_'articles.conference_registration.actions.View_Workshops'), workshops_path(conference), class: :button + - if conference.registration_status == :open && sections.include?(:schedule) - if conference.workshop_schedule_published - add_inline_script :home_schedule %h3=_'articles.workshops.headings.Schedule' = render 'conference_administration/schedule' - - else - %h3=_'articles.workshops.headings.Proposed_Workshops' - %p=_'articles.workshops.paragraphs.Proposed_Workshops' - = render 'workshops/workshop_previews', workshops: (conference.workshops.sort { |a, b| a.title.downcase <=> b.title.downcase }) - .actions.center - = link_to (_'actions.workshops.create'), create_workshop_path(conference.slug), class: [:button, :modify] - diff --git a/app/views/workshops/_show.html.haml b/app/views/workshops/_show.html.haml index 381275d..ca2d29e 100644 --- a/app/views/workshops/_show.html.haml +++ b/app/views/workshops/_show.html.haml @@ -14,39 +14,40 @@ .actions.center - translations_available_for_editing.each do |locale| = link_to (_'actions.workshops.Translate', "Translate into #{language_name(locale)}", :vars => {:language => language_name(locale)}), translate_workshop_url(workshop.conference.slug, workshop.id, locale), :class => [:button, :translate] - = columns(medium: 6) do - %h3=_'articles.workshops.headings.facilitators' - .facilitators - - workshop.workshop_facilitators.each do |f| - - u = User.find(f.user_id) - - is_this_user = (f.user_id == current_user.id) - - if logged_in? && (workshop.public_facilitator?(u) || is_this_user || is_facilitator) - .facilitator - .name=_!u.name - .role - =_"roles.workshops.facilitator.#{workshop.role(u).to_s}" - - if is_facilitator && preview.blank? - .details - .email=_!u.email - - if f.role.to_sym == :requested - =(link_to (_'actions.workshops.Approve'), approve_facilitate_workshop_request_path(workshop.conference.slug, workshop.id, f.user_id, 'approve'), :class => [:button, :modify]) - =(link_to (_'actions.workshops.Deny'), approve_facilitate_workshop_request_path(workshop.conference.slug, workshop.id, f.user_id, 'deny'), :class => [:button, :delete]) - - elsif workshop.can_remove?(current_user, u) - =(link_with_confirmation (_'actions.workshops.Make_Owner'), (_'modals.workshops.facilitators.confirm_transfer_ownership', vars: { user_name: u.name}),approve_facilitate_workshop_request_path(workshop.conference.slug, workshop.id, f.user_id, 'switch_ownership'), :class => [:button, :modify]) unless f.role.to_sym == :creator || !workshop.creator?(current_user) - =(link_with_confirmation (_"actions.workshops.#{is_this_user ? 'Leave' : 'Remove'}"), (_"modals.workshops.facilitators.confirm_remove#{is_this_user ? '_self' : ''}", vars: { user_name: u.name}), approve_facilitate_workshop_request_path(workshop.conference.slug, workshop.id, f.user_id, 'remove'), :class => [:button, :delete]) - - if is_this_user && workshop.requested_collaborator?(current_user) - .details - =(link_with_confirmation (_'actions.workshops.Cancel_Request'), (_'modals.workshops.facilitators.confirm_cancel_request'), approve_facilitate_workshop_request_path(workshop.conference.slug, workshop.id, f.user_id, 'remove'), :class => [:button, :delete]) - - unless preview.present? - =(link_to (_'actions.workshops.Facilitate'), facilitate_workshop_path(workshop.conference.slug, workshop.id), :class => [:button, workshop.needs_facilitators ? :accented : :subdued]) unless workshop.facilitator?(current_user) - - if is_facilitator - %h4=_'articles.workshops.headings.add_facilitator','Add a facilitator' - = form_tag workshop_add_facilitator_path(workshop.conference.slug, workshop.id), :class => 'add-facilitator mini-flex-form' do - .email-field.input-field - = email_field_tag :email, nil, required: true - = label_tag :email - = off_screen (_'forms.actions.aria.add'), 'add-new-desc' - = button :add, aria: { labelledby: 'add-new-desc' } + - if logged_in? + = columns(medium: 6) do + %h3=_'articles.workshops.headings.facilitators' + .facilitators + - workshop.workshop_facilitators.each do |f| + - u = User.find(f.user_id) + - is_this_user = (f.user_id == current_user.id) + - if logged_in? && (workshop.public_facilitator?(u) || is_this_user || is_facilitator) + .facilitator + .name=_!u.name + .role + =_"roles.workshops.facilitator.#{workshop.role(u).to_s}" + - if is_facilitator && preview.blank? + .details + .email=_!u.email + - if f.role.to_sym == :requested + =(link_to (_'actions.workshops.Approve'), approve_facilitate_workshop_request_path(workshop.conference.slug, workshop.id, f.user_id, 'approve'), :class => [:button, :modify]) + =(link_to (_'actions.workshops.Deny'), approve_facilitate_workshop_request_path(workshop.conference.slug, workshop.id, f.user_id, 'deny'), :class => [:button, :delete]) + - elsif workshop.can_remove?(current_user, u) + =(link_with_confirmation (_'actions.workshops.Make_Owner'), (_'modals.workshops.facilitators.confirm_transfer_ownership', vars: { user_name: u.name}),approve_facilitate_workshop_request_path(workshop.conference.slug, workshop.id, f.user_id, 'switch_ownership'), :class => [:button, :modify]) unless f.role.to_sym == :creator || !workshop.creator?(current_user) + =(link_with_confirmation (_"actions.workshops.#{is_this_user ? 'Leave' : 'Remove'}"), (_"modals.workshops.facilitators.confirm_remove#{is_this_user ? '_self' : ''}", vars: { user_name: u.name}), approve_facilitate_workshop_request_path(workshop.conference.slug, workshop.id, f.user_id, 'remove'), :class => [:button, :delete]) + - if is_this_user && workshop.requested_collaborator?(current_user) + .details + =(link_with_confirmation (_'actions.workshops.Cancel_Request'), (_'modals.workshops.facilitators.confirm_cancel_request'), approve_facilitate_workshop_request_path(workshop.conference.slug, workshop.id, f.user_id, 'remove'), :class => [:button, :delete]) + - unless preview.present? + =(link_to (_'actions.workshops.Facilitate'), facilitate_workshop_path(workshop.conference.slug, workshop.id), :class => [:button, workshop.needs_facilitators ? :accented : :subdued]) unless workshop.facilitator?(current_user) + - if is_facilitator + %h4=_'articles.workshops.headings.add_facilitator','Add a facilitator' + = form_tag workshop_add_facilitator_path(workshop.conference.slug, workshop.id), :class => 'add-facilitator mini-flex-form' do + .email-field.input-field + = email_field_tag :email, nil, required: true + = label_tag :email + = off_screen (_'forms.actions.aria.add'), 'add-new-desc' + = button :add, aria: { labelledby: 'add-new-desc' } - languages = JSON.parse(workshop.languages || '[]') - if languages.present? = columns(medium: 6) do @@ -66,24 +67,25 @@ = columns(medium: 12, class: 'workshop-notes') do %h3=_'articles.workshops.headings.notes','Notes' = richtext workshop.notes, 3 - = columns(medium: 12, id: :comments) do - %h3=_'articles.workshops.headings.Comments' - %ul.comments - - workshop.comments.each do |comment| - %li.comment{id: "comment-#{comment.id}"} - = comment(comment) - - sub_comments = comment.comments - - if sub_comments.present? - %ul.sub-comments.comments - - sub_comments.each do |sub_comment| - %li.sub-comment.comment{id: "comment-#{sub_comment.id}"} - = comment(sub_comment) - = form_tag workshop_comment_path(workshop.conference.slug, workshop.id) do - = hidden_field_tag :comment_id, comment.id - = textarea :reply, nil, plain: true, required: true, label: false, labelledby: "replyto-#{comment.id}" - .actions.right - = button :reply, value: :reply, data: {opens: "#comment-#{comment.id} form", focus: :textarea}, class: :small, id: "replyto-#{comment.id}" - = form_tag workshop_comment_path(workshop.conference.slug, workshop.id) do - = textarea :comment, nil, plain: true, required: true, label: false, labelledby: :add_comment - .actions.right - = button :add_comment, value: :add_comment, id: :add_comment + - if logged_in? + = columns(medium: 12, id: :comments) do + %h3=_'articles.workshops.headings.Comments' + %ul.comments + - workshop.comments.each do |comment| + %li.comment{id: "comment-#{comment.id}"} + = comment(comment) + - sub_comments = comment.comments + - if sub_comments.present? + %ul.sub-comments.comments + - sub_comments.each do |sub_comment| + %li.sub-comment.comment{id: "comment-#{sub_comment.id}"} + = comment(sub_comment) + = form_tag workshop_comment_path(workshop.conference.slug, workshop.id) do + = hidden_field_tag :comment_id, comment.id + = textarea :reply, nil, plain: true, required: true, label: false, labelledby: "replyto-#{comment.id}" + .actions.right + = button :reply, value: :reply, data: {opens: "#comment-#{comment.id} form", focus: :textarea}, class: :small, id: "replyto-#{comment.id}" + = form_tag workshop_comment_path(workshop.conference.slug, workshop.id) do + = textarea :comment, nil, plain: true, required: true, label: false, labelledby: :add_comment + .actions.right + = button :add_comment, value: :add_comment, id: :add_comment diff --git a/config/initializers/sorcery.rb b/config/initializers/sorcery.rb index c32d74e..28852d2 100644 --- a/config/initializers/sorcery.rb +++ b/config/initializers/sorcery.rb @@ -231,7 +231,7 @@ Rails.application.config.sorcery.configure do |config| # How long in seconds the session length will be # Default: `604800` # - user.remember_me_for = 2592000 + user.remember_me_for = 31556926 # -- user_activation -- diff --git a/config/locales/en.yml b/config/locales/en.yml index 1b866f1..4c01d5b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1569,6 +1569,8 @@ en: registrations: Modify Registrations broadcast: Contact Users broadcast_sent: Message Sent + check_in: Check In + check_in_user: Check in %{name} description: Open or close registration, view registration statistics, modify information submitted by registratnts and contact users. descriptions: @@ -1579,6 +1581,7 @@ en: process. broadcast: Send emails to targeted subsets of users. broadcast_sent: Your message has been sent. + check_in: Check in attendees to the conference broadcast: heading: Broadcast description: The broadcast tool is used to contact users through email. You @@ -2124,8 +2127,10 @@ en: registration_status: unregistered: Unregistered preregistered: Preregistered + incomplete: Incomplete registered: Registered cancelled: Cancelled + checked_in: Checked in companion: Companion companion_email: Companion Email Preferred_Languages: Language @@ -2364,6 +2369,7 @@ en: no_file_selected: No file selected actions: generic: + check_in: Complete check in reopen_registration: Re-open my registration cancel_registration: Cancel my registration organization_none: None of the above diff --git a/config/routes.rb b/config/routes.rb index 1095a73..71da3d6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -29,6 +29,7 @@ BikeBike::Application.routes.draw do post 'update/:step' => 'conference_administration#admin_update', as: :administration_update get 'events/edit/:id' => 'conference_administration#edit_event', as: :edit_event get 'locations/edit/:id' => 'conference_administration#edit_location', as: :edit_location + get 'check_in/:id' => 'conference_administration#check_in', as: :check_in, constraints: { id: /.+/ } end # Workshops diff --git a/features/schedule.feature b/features/schedule.feature index f6a4020..72d1ef9 100644 --- a/features/schedule.feature +++ b/features/schedule.feature @@ -32,18 +32,9 @@ Feature: Conference Schedule | Bike Sharing! | Recycled bike art | | Classes, Workshops, Space | Software developers exchange | - And the workshop schedule is not published + And the workshop schedule is published And I am on the conference page - Then I should see 'Bike!Bike! 2025' - And see 'Proposed Workshops' - And see 'Bike Sharing!' - But I should not see 'Schedule' - And not see 'Tuesday' - And I should see 16 workshops under 'Proposed Workshops' - - When the workshop schedule is published - And I refresh the page Then I should see 'Schedule' And see 'Tuesday' And see 'Wednesday'