From 42205d4d87b87529962ef44d8c3d6370a20d031e Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 22 Jan 2018 21:50:45 -0600 Subject: [PATCH] first attempt at onboarder interface --- admin/app/admin/jam_ruby_users.rb | 250 ++++++----- admin/app/admin/onboarding.rb | 85 ++++ admin/app/admin/students.rb | 3 +- admin/app/assets/javascripts/active_admin.js | 2 - .../assets/javascripts/active_admin.js.coffee | 1 + admin/app/assets/javascripts/application.js | 2 - admin/app/views/admin/users/_form.html.slim | 1 + db/manifest | 3 +- db/up/onboarding.sql | 22 + .../jam_ruby/models/lesson_package_type.rb | 4 + ruby/lib/jam_ruby/models/lesson_session.rb | 24 +- ruby/lib/jam_ruby/models/user.rb | 69 +++ web/app/assets/javascripts/accounts.js | 7 + web/app/assets/javascripts/jam_rest.js | 35 ++ .../AccountOnboarderScreen.js.jsx.coffee | 424 ++++++++++++++++++ .../TeacherSearchScreen.js.jsx.coffee | 7 +- .../AccountOnboarderScreen.scss | 316 +++++++++++++ web/app/controllers/api_users_controller.rb | 36 +- web/app/controllers/landings_controller.rb | 2 +- web/app/views/api_users/onboardings.rabl | 3 + web/app/views/api_users/show.rabl | 2 +- web/app/views/api_users/show_onboarding.rabl | 3 + .../views/api_users/update_onboarding.rabl | 3 + web/app/views/clients/_account.html.erb | 16 + .../clients/_account_onboarder.html.slim | 9 + web/app/views/clients/index.html.erb | 1 + web/config/routes.rb | 5 + web/spec/features/book_test_drive_spec.rb | 131 ++++++ web/spec/support/lessons.rb | 16 +- 29 files changed, 1360 insertions(+), 122 deletions(-) create mode 100644 admin/app/admin/onboarding.rb create mode 100644 db/up/onboarding.sql create mode 100644 web/app/assets/javascripts/react-components/AccountOnboarderScreen.js.jsx.coffee create mode 100644 web/app/assets/stylesheets/client/react-components/AccountOnboarderScreen.scss create mode 100644 web/app/views/api_users/onboardings.rabl create mode 100644 web/app/views/api_users/show_onboarding.rabl create mode 100644 web/app/views/api_users/update_onboarding.rabl create mode 100644 web/app/views/clients/_account_onboarder.html.slim diff --git a/admin/app/admin/jam_ruby_users.rb b/admin/app/admin/jam_ruby_users.rb index 28c331287..2c4029549 100644 --- a/admin/app/admin/jam_ruby_users.rb +++ b/admin/app/admin/jam_ruby_users.rb @@ -8,117 +8,166 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do config.sort_order = 'created_at DESC' - filter :email - filter :first_name - filter :last_name + filter :jamuser_full_name_or_email_cont, label: 'Name Or Email', as: :string filter :created_at filter :updated_at form :partial => "form" show do |user| - attributes_table do - row :id - row :email - row :admin - row :updated_at - row :created_at - row :musician - row :city - row :state - row :country - row :first_name - row :last_name - row :birth_date - row :gender - row :email_confirmed - row :remember_token - row "Session Ready" do |user| - div do - if user.ready_for_session_at - span do - 'YES' - end - span do - br - end - span do - link_to("mark as not checked", mark_session_not_ready_admin_student_path(user.id), {confirm: "Mark as not ready for session?"}) - end - else - span do - 'NO' - end - span do - br - end - span do - link_to("mark as checked", mark_session_ready_admin_student_path(user.id), {confirm: "Mark as ready for session?"}) - end + panel "Common" do + attributes_table do + row :id + row :email + row :admin + row :updated_at + row :created_at + row :musician + row :city + row :state + row :country + row :first_name + row :last_name + row :birth_date + row :gender + row :email_confirmed + row :remember_token + row "Session Ready" do |user| + div do + if user.ready_for_session_at + span do + 'YES' + end + span do + br + end + span do + link_to("mark as not checked", mark_session_not_ready_admin_student_path(user.id), {confirm: "Mark as not ready for session?"}) + end + else + span do + 'NO' + end + span do + br + end + span do + link_to("mark as checked", mark_session_ready_admin_student_path(user.id), {confirm: "Mark as ready for session?"}) + end + end end end - end - row :jamclass_credits - row :via_amazon - row "Web Profile" do - link_to "Link", "#{Rails.application.config.external_root_url}/client#/profile/#{user.id}" - end - row :image do user.photo_url ? image_tag(user.photo_url) : '' end - row "Taken Lessons" do - table_for user.taken_lessons.order('created_at desc') do - column "View" do |lesson_session| link_to("View", lesson_session.admin_url) end - column :created_at - column :status - column "Teacher" do |lesson_session| - teacher = lesson_session.teacher - span do - link_to teacher.admin_name, teacher.admin_url - end - end - column "Start Time" do |lesson_session| - span do - lesson_session.music_session.pretty_scheduled_start(true) - end - br - span do - lesson_session.music_session.scheduled_start - end - end + row :jamclass_credits + row :via_amazon + row "Web Profile" do + link_to "Link", "#{Rails.application.config.external_root_url}/client#/profile/#{user.id}" end - end - row "Taught Lessons" do - table_for user.taught_lessons.order('created_at desc') do - column "View" do |lesson_session| link_to("View", lesson_session.admin_url) end - column :created_at - column :status - column "Student" do |lesson_session| - student = lesson_session.student - span do - link_to student.admin_name, student.admin_name - end - end - column "Start Time" do |lesson_session| - span do - lesson_session.music_session.pretty_scheduled_start(true) - end - br - span do - lesson_session.music_session.scheduled_start - end - end - + row :image do + user.photo_url ? image_tag(user.photo_url) : '' end end + end + panel "Onboarding" do + attributes_table do + row :onboarding_status + row "Support Consultant" do |user| + if user.onboarder + link_to "#{user.onboarder.name} (#{user.onboarder.onboarding_users.count})", user.onboarder.admin_url + else + end + end + row "Signup" do user.created_at.to_date end + row "Assigned", :onboarder_assigned_at + row "Email 1", :onboarding_email_1_sent_at + row "Email 2", :onboarding_email_2_sent_at + row "Email 3", :onboarding_email_3_sent_at + row "Email 4", :onboarding_email_4_sent_at + row "Email 5", :onboarding_email_5_sent_at + row "Test Session Scheduled Time", :onboarding_test_session_at + row "When Test Session Was Requested", :onboarding_test_session_at + row "Test Session Outcome", :onboarding_test_session_outcome + row "Notes", :onboarding_onboarder_notes + row "Lost Reason", :onboarding_lost_reason + row "Lost At", :onboarding_lost_at + row "Escalated Reason", :onboarding_escalation_reason + row "Escalated At", :onboarding_escalated_at + + end + end + panel "Lessons" do + attributes_table do + row "Taken Lessons" do + table_for user.taken_lessons.order('created_at desc') do + column "View" do |lesson_session| + link_to("View", lesson_session.admin_url) + end + column :created_at + column :status + column "Teacher" do |lesson_session| + teacher = lesson_session.teacher + span do + link_to teacher.admin_name, teacher.admin_url + end + end + column "Start Time" do |lesson_session| + span do + lesson_session.music_session.pretty_scheduled_start(true) + end + br + span do + lesson_session.music_session.scheduled_start + end + end + + end + end + row "Taught Lessons" do + table_for user.taught_lessons.order('created_at desc') do + column "View" do |lesson_session| + link_to("View", lesson_session.admin_url) + end + column :created_at + column :status + column "Student" do |lesson_session| + student = lesson_session.student + span do + link_to student.admin_name, student.admin_name + end + end + column "Start Time" do |lesson_session| + span do + lesson_session.music_session.pretty_scheduled_start(true) + end + br + span do + lesson_session.music_session.scheduled_start + end + end + + end + end + + end + + end active_admin_comments end + index do + # actions # use this for all view/edit/delete links + column "Actions" do |user| + links = ''.html_safe + links << link_to("View", resource_path(user), :class => "member_link view_link") + links << link_to("Edit", edit_resource_path(user), :class => "member_link edit_link") + links + end column "ID" do |user| - link_to(truncate(user.id, {:length => 12}), - resource_path(user), + link_to(truncate(user.id, {:length => 12}), + resource_path(user), {:title => user.id}) end column "Email" do |user| @@ -127,7 +176,9 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do column :admin column :updated_at column :created_at - column :musician do |user| user.musician? ? true : false end + column :musician do |user| + user.musician? ? true : false + end column :city column :state column :country @@ -139,14 +190,6 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do column :photo_url column :session_settings column :can_invite - - # actions # use this for all view/edit/delete links - column "Actions" do |user| - links = ''.html_safe - links << link_to("View", resource_path(user), :class => "member_link view_link") - links << link_to("Edit", edit_resource_path(user), :class => "member_link edit_link") - links - end end controller do @@ -177,7 +220,8 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do @user = resource @user.email = params[:jam_ruby_user][:email] @user.admin = params[:jam_ruby_user][:admin] - @user.musician = params[:jam_ruby_user][:musician] + @user.is_onboarder = params[:jam_ruby_user][:is_onboarder] + @user.musician = params[:jam_ruby_user][:musician] @user.first_name = params[:jam_ruby_user][:first_name] @user.last_name = params[:jam_ruby_user][:last_name] @user.state = params[:jam_ruby_user][:state] @@ -199,7 +243,7 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do if params[:jam_ruby_user][:configure_video_no_show].to_i == 1 - @user.mod_merge({User::MOD_NO_SHOW => {User::CONFIGURE_VIDEO_NOSHOW=> true}}) + @user.mod_merge({User::MOD_NO_SHOW => {User::CONFIGURE_VIDEO_NOSHOW => true}}) else @user.delete_mod(User::MOD_NO_SHOW, User::CONFIGURE_VIDEO_NOSHOW) end diff --git a/admin/app/admin/onboarding.rb b/admin/app/admin/onboarding.rb new file mode 100644 index 000000000..55828daee --- /dev/null +++ b/admin/app/admin/onboarding.rb @@ -0,0 +1,85 @@ +ActiveAdmin.register JamRuby::User, :as => 'CurrentlyOnboarding' do + + menu :label => 'Currently Onboarding', :parent => 'JamClass' + + config.sort_order = 'created_at desc' + config.batch_actions = true + config.per_page = 100 + config.paginate = true + config.filters = true + config.clear_action_items! + batch_action :destroy, false + + batch_action :onboarder, form: { + support_consultant: (User.where(is_onboarder: true).includes(:onboarding_users).map {|user| ["#{user.name} (#{user.onboarding_users.length})", user.id]}).to_a.unshift(['Unassign', '']) + } do |ids, inputs| + onboarder = inputs[:support_consultant] + if onboarder.blank? + onboarder = nil + else + onboarder = User.find(onboarder) + end + + ids.each do |id| + user = User.find(id) + user.onboarder = onboarder + user.onboarder_assigned_at = Date.today + user.save + end + + if onboarder + msg = 'Assigned ' + User.find(onboarder).name + " to #{ids.length} users" + + else + msg = "Unassigned any Support Consultant from #{ids.length} users" + end + + redirect_to :back, notice: msg + end + + filter :jamuser_full_name_or_email_cont, label: 'Name Or Email', as: :string + filter :onboarder, as: :select, :collection => User.where(is_onboarder: true), label: 'Support Consultant' + filter :onboarder_id_blank, :as => :boolean, label: 'Unassigned' + filter :onboarding_escalation_reason_present, :as => :boolean, label: 'Escalated' + scope("TestDrive/Amazon Users", default: true) { |scope| scope.joins(:posa_cards).where('posa_cards.lesson_package_type_id in (?)', LessonPackageType::AMAZON_PACKAGES + LessonPackageType::LESSON_PACKAGE_TYPES) } + + + controller do + active_admin_config.includes.push :onboarding_users + end + + index do + selectable_column + column "Name" do |user| + link_to user.name, user.admin_url + end + column :email + column :onboarding_status + column "Lost Reason", :onboarding_lost_reason + column "Escalated Reason", :onboarding_escalation_reason + column "Support Consultant" do |user| + if user.onboarder + link_to "#{user.onboarder.name} (#{user.onboarder.onboarding_users.count})", user.onboarder.admin_url + else + end + end + + column "Signup" do |user| + user.created_at.to_date + end + column "Assigned", :onboarder_assigned_at + column "Email 1", :onboarding_email_1_sent_at + column "Email 2", :onboarding_email_2_sent_at + column "Email 3", :onboarding_email_3_sent_at + column "Email 4", :onboarding_email_4_sent_at + column "Email 5", :onboarding_email_5_sent_at + column "Test Session", :onboarding_test_session_scheduled_at + end + + member_action :update_onboarder, :method => :post do + resource.onboarder = params[:onboarder] + resource.save! + redirect_to :back + end + +end \ No newline at end of file diff --git a/admin/app/admin/students.rb b/admin/app/admin/students.rb index dc28943d2..1e755f21f 100644 --- a/admin/app/admin/students.rb +++ b/admin/app/admin/students.rb @@ -15,7 +15,8 @@ ActiveAdmin.register JamRuby::User, :as => 'Students' do filter :jamuser_full_name_or_email_cont, label: 'Name Or Email', as: :string filter :school, label: 'School' - scope("Default", default: true) { |scope| scope.where('is_a_student = true OR jamclass_credits > 0 OR ((select count(id) from lesson_bookings where lesson_bookings.user_id = users.id) > 0)').order('users.ready_for_session_at IS NULL DESC') } + scope("TestDrive/Amazon Users", default: true) {|scope| scope.joins(:posa_cards).where('posa_cards.lesson_package_type_id in (?)', LessonPackageType::AMAZON_PACKAGES + LessonPackageType::LESSON_PACKAGE_TYPES) } + scope("Student Or Has Credits Or Has Lesson") { |scope| scope.where('is_a_student = true OR jamclass_credits > 0 OR ((select count(id) from lesson_bookings where lesson_bookings.user_id = users.id) > 0)').order('users.ready_for_session_at IS NULL DESC') } index do column "Name" do |user| diff --git a/admin/app/assets/javascripts/active_admin.js b/admin/app/assets/javascripts/active_admin.js index 889722595..84a187b5a 100644 --- a/admin/app/assets/javascripts/active_admin.js +++ b/admin/app/assets/javascripts/active_admin.js @@ -1,6 +1,4 @@ //= require active_admin/base -//= require jquery3 -//= require jquery_ujs //= require activeadmin_addons/all // //= require jquery-ui diff --git a/admin/app/assets/javascripts/active_admin.js.coffee b/admin/app/assets/javascripts/active_admin.js.coffee index 3752dcef6..e211bdfe7 100644 --- a/admin/app/assets/javascripts/active_admin.js.coffee +++ b/admin/app/assets/javascripts/active_admin.js.coffee @@ -1 +1,2 @@ #= require active_admin/base +#= require jquery3 diff --git a/admin/app/assets/javascripts/application.js b/admin/app/assets/javascripts/application.js index fb7cad79b..e8dfcfbc6 100644 --- a/admin/app/assets/javascripts/application.js +++ b/admin/app/assets/javascripts/application.js @@ -10,5 +10,3 @@ // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD // GO AFTER THE REQUIRES BELOW. // -//= require jquery -//= require jquery_ujs diff --git a/admin/app/views/admin/users/_form.html.slim b/admin/app/views/admin/users/_form.html.slim index f8803324d..2e8d483b9 100644 --- a/admin/app/views/admin/users/_form.html.slim +++ b/admin/app/views/admin/users/_form.html.slim @@ -2,6 +2,7 @@ = f.inputs "Details" do = f.input :email, label: 'Email' = f.input :admin + = f.input :is_onboarder, label: 'Is Support Consultant' = f.input :first_name = f.input :last_name = f.input :city diff --git a/db/manifest b/db/manifest index d6498f455..46eecb8d1 100755 --- a/db/manifest +++ b/db/manifest @@ -380,4 +380,5 @@ amazon_v1.sql sms_index_optimize.sql amazon_signup.sql age_out_sessions.sql -alter_crash_dumps.sql \ No newline at end of file +alter_crash_dumps.sql +onboarding.sql \ No newline at end of file diff --git a/db/up/onboarding.sql b/db/up/onboarding.sql new file mode 100644 index 000000000..73f11b95f --- /dev/null +++ b/db/up/onboarding.sql @@ -0,0 +1,22 @@ +ALTER TABLE users ADD COLUMN is_onboarder BOOLEAN NOT NULL DEFAULT FALSE; + +ALTER TABLE users ADD COLUMN onboarding_status VARCHAR DEFAULT 'Unassigned'; +ALTER TABLE users ADD COLUMN onboarding_lost_reason VARCHAR; +ALTER TABLE users ADD COLUMN onboarding_lost_at DATE; +ALTER TABLE users ADD COLUMN onboarding_escalation_reason VARCHAR; +ALTER TABLE users ADD COLUMN onboarding_escalated_at date; +ALTER TABLE users ADD COLUMN onboarder_id VARCHAR(64) REFERENCES users ON DELETE SET NULL; +ALTER TABLE users ADD COLUMN onboarder_assigned_at DATE; +ALTER TABLE users ADD COLUMN onboarding_email_1_sent_at DATE; +ALTER TABLE users ADD COLUMN onboarding_email_2_sent_at DATE; +ALTER TABLE users ADD COLUMN onboarding_email_3_sent_at DATE; +ALTER TABLE users ADD COLUMN onboarding_email_4_sent_at DATE; +ALTER TABLE users ADD COLUMN onboarding_email_5_sent_at DATE; +ALTER TABLE users ADD COLUMN onboarding_test_session_scheduled_at DATE; +ALTER TABLE users ADD COLUMN onboarding_test_session_at timestamp without time zone; +ALTER TABLE users ADD COLUMN onboarding_test_session_outcome VARCHAR; +ALTER TABLE users ADD COLUMN onboarding_onboarded_at DATE; +ALTER TABLE users ADD COLUMN onboarding_onboarder_notes VARCHAR; +ALTER TABLE users ADD COLUMN first_onboarding_free_lesson_at timestamp without time zone; +ALTER TABLE users ADD COLUMN first_onboarding_paid_lesson_at timestamp without time zone; + diff --git a/ruby/lib/jam_ruby/models/lesson_package_type.rb b/ruby/lib/jam_ruby/models/lesson_package_type.rb index 85d115829..6bea27671 100644 --- a/ruby/lib/jam_ruby/models/lesson_package_type.rb +++ b/ruby/lib/jam_ruby/models/lesson_package_type.rb @@ -116,6 +116,10 @@ module JamRuby description(lesson_booking) end + def is_free? + # if it's a 0 dollar test drive, or the 'single free' lesson, then it's atually free + (is_test_drive && price == 0.00) || is_single_free + end def is_single_free? id == SINGLE_FREE end diff --git a/ruby/lib/jam_ruby/models/lesson_session.rb b/ruby/lib/jam_ruby/models/lesson_session.rb index a4feb1010..df95fc99e 100644 --- a/ruby/lib/jam_ruby/models/lesson_session.rb +++ b/ruby/lib/jam_ruby/models/lesson_session.rb @@ -173,7 +173,7 @@ module JamRuby # give 2 days to auto-cancel; this lets people just jump in a session and mark it done def self.auto_cancel - MusicSession.joins(lesson_session: :lesson_booking).where('lesson_sessions.status = ? OR lesson_bookings.status = ?', LessonSession::STATUS_REQUESTED, LessonBooking::STATUS_COUNTERED).where("? > scheduled_start + (INTERVAL '2 days' * (duration))", Time.now).each do |music_session| + MusicSession.joins(lesson_session: :lesson_booking).where('lesson_sessions.status = ? OR lesson_bookings.status = ?', LessonSession::STATUS_REQUESTED, LessonBooking::STATUS_COUNTERED).where("? > scheduled_start + (INTERVAL '2 days') + (INTERVAL '1 minutes' * (duration))", Time.now).each do |music_session| lesson_session = music_session.lesson_session lesson_session.autocancel end @@ -218,6 +218,10 @@ module JamRuby counterer_id.nil? || counterer_id == student_id end + def lesson_is_free? + lesson_booking.booked_price == 0.00 + end + def mark_lesson(success, administratively_marked = false) if !self.success && (is_requested? || is_unconfirmed?) # what's going on here is that we will just mark a session as a success if the the people got in in case of requested sessions @@ -291,6 +295,24 @@ module JamRuby return end + + # record how it went + if analysis[:student_analysis] && analysis[:student_analysis][:total_time] > 0 + if lesson_is_free? + if student.first_onboarding_free_lesson_at.nil? + student.first_onboarding_free_lesson_at = Time.now + student.save + end + else + if student.first_onboarding_paid_lesson_at.nil? + student.first_onboarding_paid_lesson_at = Time.now + student.save + end + + end + + end + mark_lesson(analysis[:bill]) end diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index a3e805bb5..f85b2ae82 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -32,6 +32,38 @@ module JamRuby MINIMUM_AUDIO_LATENCY = 2 MAXIMUM_AUDIO_LATENCY = 10000 + ONBOARDING_STATUS_UNASSIGNED = "Unassigned" + ONBOARDING_STATUS_ASSIGNED = "Assigned" + ONBOARDING_STATUS_EMAILED = "Emailed" + ONBOARDING_STATUS_ONBOARDED = "Onboarded" + ONBOARDING_STATUS_LOST = "Lost" + ONBOARDING_STATUS_ESCALATED = "Escalated" + ONBOARDING_STATUS_FREE_LESSON = "Free Lesson Taken" + ONBOARDING_STATUS_PAID_LESSON = "Paid Lesson Taken" + ONBOARDING_STATUES = [ONBOARDING_STATUS_UNASSIGNED, ONBOARDING_STATUS_ASSIGNED, ONBOARDING_STATUS_EMAILED, ONBOARDING_STATUS_ONBOARDED, ONBOARDING_STATUS_LOST, ONBOARDING_STATUS_ESCALATED, ONBOARDING_STATUS_FREE_LESSON, ONBOARDING_STATUS_PAID_LESSON ] + SESSION_OUTCOME_SUCCESSFUL = "Successful" + SESSION_OUTCOME_SETUP_WIZARD_FAILURE = "Setup Wizard Failure" + SESSION_OUTCOME_NO_AUDIO_STREAM = "No Audio Stream in Session" + SESSION_OUTCOME_NO_VIDEO_STREAM = "No Video Stream In Session" + SESSION_OUTCOME_OTHER = "Other" + SESSION_OUTCOMES = [SESSION_OUTCOME_SUCCESSFUL, SESSION_OUTCOME_SETUP_WIZARD_FAILURE, SESSION_OUTCOME_NO_AUDIO_STREAM, SESSION_OUTCOME_NO_VIDEO_STREAM, SESSION_OUTCOME_OTHER] + LOST_REASON_LOST_INTEREST = "Lost Interest" + LOST_REASON_NO_COMPUTER = "No Win/Mac Computer" + LOST_REASON_NO_BROADBAND = "No Broadband Internet" + LOST_REASON_NO_WEBCAM = "No Webcam" + LOST_REASON_BAD_INTERNET = "Bad Internet" + LOST_REASON_SETUP_WIZARD_FAILURE = "Setup Wizard Failure" + LOST_REASON_NO_AUDIO_STREAM = "No Audio Stream In Session" + LOST_REASON_NO_VIDEO_STREAM = "No Video Stream In Session" + LOST_REASON_OTHER = "Other" + LOST_REASONS = [LOST_REASON_LOST_INTEREST, LOST_REASON_NO_COMPUTER, LOST_REASON_NO_BROADBAND, LOST_REASON_NO_WEBCAM, LOST_REASON_BAD_INTERNET, LOST_REASON_SETUP_WIZARD_FAILURE, LOST_REASON_NO_AUDIO_STREAM, LOST_REASON_NO_VIDEO_STREAM, LOST_REASON_OTHER] + ESCALATION_REASON_NO_AUDIO_STREAM = "No Audio Stream In Session" + ESCALATION_REASON_NO_VIDEO_STREAM = "No Video Stream In Session" + ESCALATION_REASON_SETUP_WIZARD_FAILURE = "Setup Wizard Failure" + ESCALATION_REASON_OTHER = "Other" + ESCALATION_REASONS = [ESCALATION_REASON_NO_AUDIO_STREAM, ESCALATION_REASON_NO_VIDEO_STREAM, ESCALATION_REASON_SETUP_WIZARD_FAILURE, ESCALATION_REASON_OTHER] + + devise :database_authenticatable, :recoverable, :rememberable acts_as_mappable @@ -203,6 +235,8 @@ module JamRuby has_many :taken_lessons, :class_name => "JamRuby::LessonSession", inverse_of: :user, foreign_key: :user_id has_many :taught_lessons, :class_name => "JamRuby::LessonSession", inverse_of: :teacher, foreign_key: :teacher_id belongs_to :school, :class_name => "JamRuby::School", inverse_of: :students + belongs_to :onboarder, :class_name => "JamRuby::User", inverse_of: :onboarding_users, foreign_key: :onboarder_id + has_many :onboarding_users, :class_name => "JamRuby::User", inverse_of: :onboarder, foreign_key: :onboarder_id has_one :owned_school, :class_name => "JamRuby::School", inverse_of: :user has_one :owned_retailer, :class_name => "JamRuby::Retailer", inverse_of: :user has_many :test_drive_package_choices, :class_name =>"JamRuby::TestDrivePackageChoice" @@ -239,6 +273,7 @@ module JamRuby validates :show_whats_next, :inclusion => {:in => [nil, true, false]} validates :is_a_student, :inclusion => {:in => [true, false]} validates :is_a_teacher, :inclusion => {:in => [true, false]} + validates :is_onboarder, :inclusion => {:in => [true, false, nil]} #validates :mods, json: true validates_numericality_of :last_jam_audio_latency, greater_than: MINIMUM_AUDIO_LATENCY, less_than: MAXIMUM_AUDIO_LATENCY, :allow_nil => true validates :last_jam_updated_reason, :inclusion => {:in => [nil, JAM_REASON_REGISTRATION, JAM_REASON_NETWORK_TEST, JAM_REASON_FTUE, JAM_REASON_JOIN, JAM_REASON_IMPORT, JAM_REASON_LOGIN]} @@ -287,6 +322,16 @@ module JamRuby retailer.save! end end + + if onboarding_lost_reason_changed? || + onboarding_escalation_reason_changed? || + onboarder_id_changed? || + onboarding_email_5_sent_at_changed? || + first_onboarding_free_lesson_at_changed? || + first_onboarding_paid_lesson_at_changed? || + onboarding_onboarded_at + User.where(id: self.id).update_all(onboarding_status: self.computed_onboarding_status) + end end def update_teacher_pct if teacher @@ -2006,6 +2051,10 @@ module JamRuby APP_CONFIG.admin_root_url + "/admin/students" # should add id; not yet supported end + def admin_onboarding_url + APP_CONFIG.admin_root_url + "/admin/currently_onboardings" + end + def jam_track_rights_admin_url APP_CONFIG.admin_root_url + "/admin/jam_track_rights?q[user_id_equals]=#{id}&commit=Filter&order=created_at DESC" end @@ -2443,6 +2492,26 @@ module JamRuby student.school && self.teacher && self.teacher.school && student.school.id == self.teacher.school.id end + def computed_onboarding_status + if first_onboarding_paid_lesson_at + ONBOARDING_STATUS_PAID_LESSON + elsif first_onboarding_free_lesson_at + ONBOARDING_STATUS_FREE_LESSON + elsif onboarding_onboarded_at + ONBOARDING_STATUS_ONBOARDED + elsif onboarding_lost_reason + ONBOARDING_STATUS_LOST + elsif onboarding_escalation_reason + ONBOARDING_STATUS_ESCALATED + elsif onboarding_email_5_sent_at + ONBOARDING_STATUS_EMAILED + elsif onboarder_id + ONBOARDING_STATUS_ASSIGNED + else + ONBOARDING_STATUS_UNASSIGNED + end + end + private def create_remember_token self.remember_token = SecureRandom.urlsafe_base64 diff --git a/web/app/assets/javascripts/accounts.js b/web/app/assets/javascripts/accounts.js index b7025e1ea..132d371e0 100644 --- a/web/app/assets/javascripts/accounts.js +++ b/web/app/assets/javascripts/accounts.js @@ -82,6 +82,7 @@ affiliate_referral_count: userDetail.affiliate_referral_count, owns_school: !!userDetail.owned_school_id, owns_retailer: !!userDetail.owned_retailer_id, + is_onboarder: userDetail.is_onboarder, webcamName: webcamName } , { variable: 'data' })); @@ -148,6 +149,7 @@ $("#account-content-scroller").on('click', '#account-affiliate-partner-link', function(evt) {evt.stopPropagation(); navToAffiliates(); return false; } ); $("#account-content-scroller").on('click', '#account-school-link', function(evt) {evt.stopPropagation(); navToSchool(); return false; } ); $("#account-content-scroller").on('click', '#account-retailer-link', function(evt) {evt.stopPropagation(); navToRetailer(); return false; } ); + $("#account-content-scroller").on('click', '#account-onboarder-link', function(evt) {evt.stopPropagation(); navToOnboarder(); return false; } ); } function renderAccount() { @@ -215,6 +217,11 @@ window.location = '/client#/account/retailer' } + function navToOnboarder() { + resetForm() + window.location = '/client#/account/onboarder' + } + // handle update avatar event function updateAvatar(avatar_url) { var photoUrl = context.JK.resolveAvatarUrl(avatar_url); diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index fb7723942..36d3d98d4 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -2812,6 +2812,38 @@ }) } + function listOnboardings(id) { + return $.ajax({ + type: "GET", + url: '/api/users/' + id + '/onboardings', + dataType: "json", + contentType: 'application/json' + }) + } + + function getOnboarding(id) { + return $.ajax({ + type: "GET", + url: '/api/users/' + id + '/onboardings', + dataType: "json", + contentType: 'application/json' + }) + } + function updateOnboarding(options) { + options = options || {} + var id = options.id + delete options.id + + return $.ajax({ + type: 'POST', + url: '/api/users/' + id + '/onboardings', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function initialize() { return self; @@ -3062,6 +3094,9 @@ this.liveStreamTransition = liveStreamTransition; this.getLiveStream = getLiveStream; this.getBroadcast = getBroadcast; + this.listOnboardings = listOnboardings; + this.getOnboarding = getOnboarding; + this.updateOnboarding = updateOnboarding; return this; }; })(window, jQuery); diff --git a/web/app/assets/javascripts/react-components/AccountOnboarderScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/AccountOnboarderScreen.js.jsx.coffee new file mode 100644 index 000000000..d9a45305a --- /dev/null +++ b/web/app/assets/javascripts/react-components/AccountOnboarderScreen.js.jsx.coffee @@ -0,0 +1,424 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +AppStore = context.AppStore +UserStore = context.UserStore + +profileUtils = context.JK.ProfileUtils + +@AccountOnboarderScreen = React.createClass({ + + mixins: [ + Reflux.listenTo(AppStore, "onAppInit"), + Reflux.listenTo(UserStore, "onUserChanged") + ] + + shownOnce: false + screenVisible: false + updates: [] + + onAppInit: (@app) -> + @app.bindScreen('account/onboarder', {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide}) + + onRetailerChanged: (retailerState) -> + @setState(retailerState) + + onUserChanged: (userState) -> + @getOnboardings(userState?.user) + @setState({user: userState?.user}) + + componentDidMount: () -> + @root = $(@getDOMNode()) + + componentDidUpdate: () -> + datePicked = @datePicked.bind(this) + @sessionDate = @root.find('.date-picker') + @sessionDate.each(() -> + $this = $(this) + id = $this.attr('data-id') + day = $this.attr('data-date') + console.log("id-day: #{id}-#{day}") + + $this.datepicker({ + dateFormat: "D M d yy", + onSelect: ((e, inst) -> + datePicked(id, 'onboarding_test_session_at') + ) + }) + + # initialize day + if(day) + $this.datepicker("setDate", new Date(day)) + ) + + + toggleDate: (dateText, inst) -> + console.log("THIS", this) + console.log("IDID ID #{$(this).attr('data-id')}") + 'onboarding_test_session_at' + + beforeHide: (e) -> + @screenVisible = false + return true + + beforeShow: (e) -> + LocationActions.load() + + getOnboardings: (user) -> + if user?.id? && @screenVisible + @setState({fetchingOnboardings: true}) + rest.listOnboardings(context.JK.currentUserId).done((onboardings) => + @setState({onboardings: onboardings, fetchingOnboardings: false}) + ) + .fail(() => + @setState({fetchingOnboardings: false}) + @app.layout.notify({ + title: "onboarding list failure", + text: "Unable to list out your onboarders. Please contact support@jamkazam.com" + }) + ) + + afterShow: (e) -> + @screenVisible = true + logger.debug("AccountOnboardingScreen: afterShow") + logger.debug("after show", @state.user) + @getOnboardings(@state.user) + + getInitialState: () -> + { + onboardings: [], + user: null, + updating: false + } + + onCancel: (e) -> + e.preventDefault() + context.location.href = '/client#/account' + + onUpdate: (e) -> + if e? + e.preventDefault() + + if this.state.updating + return + + if @updates.length == 0 + return + update = @updates.pop() + + @setState({updating: true}) + rest.updateOnboarding(update).done((response) => @onUpdateDone(response)).fail((jqXHR) => @onUpdateFail(jqXHR)) + + + updateRow: (onboarding) -> + for match in this.state.onboardings + if match.id == onboarding.id + $.extend(match, onboarding) + console.log("UPDATED ONBOARDING", onboarding) + break + this.setState({onboardings: this.state.onboardings}) + + onUpdateDone: (response) -> + this.setState({updating: false}) + @updateRow(response) + if @updates.length > 0 + @onUpdate() + + onUpdateFail: (jqXHR) -> + this.setState({updating: false}) + @app.layout.notify({ + title: "update failure", + text: "Unable to update user. Please let us know which user and field you can not update." + }) + if @updates.length > 0 + @onUpdate() + + + + + updateField: (id, field, e) -> + if(e) + e.preventDefault() + + console.log("Update Field Called for ", field) + $target = $(e.target) + console.log("TARGET", $target) + value = true + if $target.is('select') + value = $target.val() + options = {id: id} + options[field] = value + @queueUpdate(options) + + queueUpdate: (update) -> + @updates.push(update) + @onUpdate() + + + createLinkField: (id, onboarding, field, display, timeLabel = null) -> + if onboarding[field] + email = `{timeLabel} {onboarding[field]}` + else + email = `{display}` + + email + + showLostBanner: (id, field) -> + lostHtml = 'Please choose a reason why the user was lost:

' + + '' + $item = context.JK.Banner.showAlert( + {buttons: [{name: 'CANCEL', click: () -> console.log('cancel clicked')}], html: lostHtml}); + $item.find('select').change((e)=> @updateField(id, field, e); context.JK.Banner.hide()) + + showEscalationBanner: (id, field) -> + lostHtml = 'Please choose a reason why the user needs escalating:

' + + '' + $item = context.JK.Banner.showAlert( + {buttons: [{name: 'CANCEL', click: () -> console.log('cancel clicked')}], html: lostHtml}); + $item.find('select').change((e)=> @updateField(id, field, e); context.JK.Banner.hide()) + + createLostField: (id, onboarding) -> + field = 'onboarding_lost_reason' + if onboarding[field] + field = `{ + onboarding[field] + }` + else + field = `lost student` + field + + createEscalatedField: (id, onboarding) -> + field = 'onboarding_escalation_reason' + if onboarding[field] + field = `{ + onboarding[field] + }` + else + field = `escalate + student` + field + + createOnboardingField: (onboarding) -> + @createLinkField(onboarding.id, onboarding, 'onboarding_onboarded_at', 'onboarded successfully') + + watchTextArea: (id, field, e) -> + $text = $(e.target) + if @textTimeout + clearTimeout(@textTimeout) + @textTimeout = null + @textTimeout = setTimeout(() => + data = {id: id} + data[field] = $text.val() + @queueUpdate(data) + @textTimeout = null + , 3000) + + datePicked: (id, field) -> + $tr = @root.find("tr[data-id='" + id + "']") + picker = $tr.find(".date-picker") + + hour = $tr.find('.hour').val() + minute = $tr.find('.minute').val() + am_pm = $tr.find('.am_pm').val() + + + if hour? and hour != '' + hour = new Number(hour) + if am_pm == 'PM' + hour += 12 + else + hour = null + + if minute? and minute != '' + minute = new Number(minute) + else + minute = null + + date = picker.datepicker("getDate") + if date? + date.setHours(hour) + date.setMinutes(minute) + data = {id: id} + data[field] = date.toString() + @queueUpdate(data) + + mainContent: () -> + cancelClasses = {"button-orange": true, "update": true} + onboardings = [] + + if @state.fetchingOnboardings + onboardings.push(` + FETCHING STUDENTS + `) + else if @state.onboardings.length > 0 + for onboarding in @state.onboardings + console.log("onboarding", onboarding) + + hours = [] + for hour in ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'] + if hour == '12' + key = '00' + else + key = hour + hours.push(``) + + minutes = [] + for minute in ['00', '15', '30', '45'] + minutes.push(``) + + am_pm = [``, ``] + + + email1 = @createLinkField(onboarding.id, onboarding, 'onboarding_email_1_sent_at', 'sent email 1', 'email 1:') + email2 = @createLinkField(onboarding.id, onboarding, 'onboarding_email_2_sent_at', 'sent email 2', 'email 2:') + email3 = @createLinkField(onboarding.id, onboarding, 'onboarding_email_3_sent_at', 'sent email 3', 'email 3:') + email4 = @createLinkField(onboarding.id, onboarding, 'onboarding_email_4_sent_at', 'sent email 4', 'email 4:') + email5 = @createLinkField(onboarding.id, onboarding, 'onboarding_email_5_sent_at', 'sent email 5', 'email 5:') + + scheduledSession = @createLinkField(onboarding.id, onboarding, 'onboarding_test_session_scheduled_at', + 'scheduled session', 'scheduled session:') + + sessionOutcomes = ['', "Successful", "Setup Wizard Failure", "No Audio Stream in Session", + "No Video Stream In Session", "Other"] + sessionOutcomesJSX = [] + for sessionOutcome in sessionOutcomes + sessionOutcomesJSX.push(``) + selectSessionOutcome = + `` + + session_at = onboarding.onboarding_test_session_at + amOrPm = 'AM' + currentHours = '01' + currentMinutes = '00' + if session_at + sessionTime = new Date(session_at) + session_at = sessionTime.toLocaleDateString() + currentHours = sessionTime.getHours() + if sessionTime.getHours() > 11 + amOrPm = 'PM' + currentHours -= 12 + + currentMinutes = sessionTime.getMinutes() + + active = onboarding.onboarding_status == 'Emailed' || onboarding.onboarding_status == 'Assigned' + + if active + activeClass = 'active-row' + else + activeClass = 'inactive-row' + + columns = [] + if active + columns.push(` +
{email1}
+
{email2}
+
{email3}
+
{email4}
+
{email5}
+ `) + columns.push(` +
{scheduledSession}
+ +
+ + + : + +
+
+ {selectSessionOutcome}
+ `) + columns.push(` +
{this.createLostField(onboarding.id, onboarding)}
+
{this.createEscalatedField(onboarding.id, onboarding)}
+ `) + columns.push(` +
{this.createOnboardingField(onboarding)}
+ `) + else + columns.push(` + {onboarding.onboarding_status}

This student will drop off your list now next time you visit this page. + `) + onboarding = ` + +
Name: {onboarding.name}
+
Email: {onboarding.email}
+
Assigned + on {onboarding.onboarder_assigned_at}
+ + {columns} + +
+ +
+ + ` + onboardings.push(onboarding) + else + onboardings.push(` + NO STUDENTS ASSIGNED + `) + + `
+
+
+ Manage the users here that you are responsible for onboarding.

+ All links in the table below, when clicked, will immediately mark that item as done. For example, once + you send your first email to the user, click the sent email 1 link.

+ All other fields will automatically save as you use them, such as the Session Outcome dropdown, + and the Notes text box. +
+ + + + + + + + + + + + + {onboardings} + +
UserEmailTest SessionProblemsOnboardedNotes
+
+ + +
+ BACK +
+
` + + render: () -> + mainContent = @mainContent() + + `
+
+
+
+ +
+
+
+ {mainContent} +
+
+
+
+
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee index 2a2e2fea4..870e1d8d8 100644 --- a/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee @@ -167,9 +167,10 @@ ProfileActions = @ProfileActions window.location.href = '/client#/jamclass/test-drive-selection/' + user.id else if response.remaining_test_drives > 0 if response.booked_with_teacher && !context.JK.currentUserAdmin - logger.debug("TeacherSearchScreen: teacher already test-drived") - - context.JK.Banner.showAlert('TestDrive', "You have already taken a TestDrive lesson from this teacher. With TestDrive, you need to use your lessons on 4 different teachers to find one who is best for you. We're sorry, but you cannot take multiple TestDrive lessons from a single teacher.") + #logger.debug("TeacherSearchScreen: teacher already test-drived") + #context.JK.Banner.showAlert('TestDrive', "You have already taken a TestDrive lesson from this teacher. With TestDrive, you need to use your lessons on 4 different teachers to find one who is best for you. We're sorry, but you cannot take multiple TestDrive lessons from a single teacher.") + logger.debug("TeacherSearchScreen: user being sent to book a lesson") + window.location.href = '/client#/jamclass/book-lesson/test-drive_' + user.id else # send on to booking screen for this teacher logger.debug("TeacherSearchScreen: user being sent to book a lesson") diff --git a/web/app/assets/stylesheets/client/react-components/AccountOnboarderScreen.scss b/web/app/assets/stylesheets/client/react-components/AccountOnboarderScreen.scss new file mode 100644 index 000000000..60cb2cce9 --- /dev/null +++ b/web/app/assets/stylesheets/client/react-components/AccountOnboarderScreen.scss @@ -0,0 +1,316 @@ +@import "client/common"; + + +#account-onboarder { + div[data-react-class="AccountOnboarderScreen"] { + height: 100%; + } + + .daystamper { + white-space: nowrap; + color:#fc0; + } + .profile-header { + padding: 10px 30px !important; + } + .info-unit { + margin:5px 10px; + font-size:12px; + + select {font-size:12px; margin-top:5px;} + textarea{font-size:12px;min-height:100px;min-width:200px;} + + } + .instructions { + margin-bottom:20px; + } + td { + vertical-align:middle; + } + td > * { + vertical-align : middle; + } + + label { + display: inline-block; + min-width: 200px; + } + input { + min-width:200px; + } + .hint { + margin-left: 200px; + font-size: 12px; + font-style: italic; + margin-top: 5px; + } + .iradio_minimal { + display: inline-block; + top: 4px; + margin-right: 5px; + } + .field { + margin-bottom: 30px; + + &.stripe-connect { + margin-bottom: 10px; + label { + margin-bottom: 10px; + } + } + } + .usage-hint { + font-size:12px; + text-decoration:underline; + margin-left:10px; + } + .scooter { + margin-bottom:10px; + } + div.retailer-split { + margin-top:10px; + } + .store-header { + float: left; + padding-top: 10px; + font-size: 20px; + font-weight: bold; + } + .profile-nav a { + position: absolute; + text-align: center; + height: 100%; + width: 98%; + margin: 0 auto; + padding: 11px 0 0 0; + @include border-box_sizing; + } + + .profile-tile { + width: 25%; + float: left; + @include border-box_sizing; + height: 40px; + position: relative; + } + .profile-body { + padding-top: 40px; + } + .profile-photo { + width: 16%; + @include border-box_sizing; + } + .profile-nav { + margin: 0; + width: 84%; + } + .profile-wrapper { + padding: 10px 20px + } + + .main-content { + float: left; + @include border-box_sizing; + width: 84%; + } + + .info-block { + min-height:400px; + h3 { + font-weight: normal; + font-size: 14px; + margin-bottom: 10px; + min-width: 200px; + display: inline-block; + } + + h4 { + margin-bottom: 10px; + } + + .section { + margin-bottom: 40px; + &.teachers { + clear: both; + } + } + + table.jamtable { + font-size: 12px; + width: 100%; + } + table.jamtable tr.inactive-row { + background-color:gray; + } + } + + .stripe-connect { + padding: 0; + border: 0; + background: transparent; + outline:transparent; + cursor:pointer; + } + + .actions { + float: left; + margin-top: 30px; + margin-bottom: 10px; + } + + a.cancel { + margin-left:3px; + } + + + .avatar-edit-link { + display:inline-block; + img { + max-width:200px; + } + } + + .avatar-edit-link { + .hint { + margin-left:0; + } + } + + .column { + width:50%; + @include border_box_sizing; + + h3 { + float:left; + } + .invite-dialog { + float:right; + margin-right:2px; + } + &.column-left { + float:left; + padding-right:30px; + + } + &.column-right { + float:right; + padding-left:30px; + } + + .username { + max-width:40%; + font-size:16px; + color:white; + } + table { + width:100%; + } + td.description { + font-size:16px; + color: white; + vertical-align: top; + white-space: nowrap; + } + td.message { + color: $ColorTextTypical; + padding-left: 10px; + vertical-align: top; + text-align:right; + } + .detail-block { + display:inline-block; + font-size:12px; + } + .resend { + float:left; + } + .delete { + float:right; + } + .teacher-invites, .student-invites { + margin-bottom: 20px; + margin-top:40px; + font-size:12px; + min-height:40px; + p { + font-size:12px; + } + } + .teachers, .students { + margin-bottom:20px; + } + p { + font-size:12px; + margin-left:0; + } + .retailer-invitation { + margin-bottom:20px; + } + } + .retailer-user { + margin-bottom:20px; + + .avatar { + position:absolute; + padding:1px; + width:32px; + height:32px; + background-color:#ed4818; + margin:0; + -webkit-border-radius:16px; + -moz-border-radius:16px; + border-radius:16px; + float:none; + } + .avatar img { + width: 32px; + height: 32px; + -webkit-border-radius:16px; + -moz-border-radius:16px; + border-radius:16px; + } + + .usersname { + margin-left:56px; + line-height:32px; + vertical-align: middle; + display: inline-block; + + } + + select[name="regions"] { + margin-botom:30px; + } + + select[name="cities"] { + margin-bottom: 30px; + } + + .just-name { + display:block; + } + .just-email { + position: relative; + top: -14px; + font-size:12px; + } + + .user-actions { + float: right; + line-height: 32px; + height: 32px; + vertical-align: middle; + font-size:12px; + } + } + p { + font-size:12px; + margin:0; + } + + .split-input { + :after { + content: '%'; + } + } +} diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index bab6cdcd1..62b9e0186 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -1,7 +1,7 @@ require 'sanitize' class ApiUsersController < ApiController - before_filter :api_signed_in_user, :except => [:create, :calendar, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump, :validate_data, :google_auth, :user_event] + before_filter :api_signed_in_user, :except => [:create, :calendar, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump, :validate_data, :google_auth, :user_event, :onboardings, :update_onboarding, :show_onboarding] before_filter :auth_user, :only => [:session_settings_show, :session_history_index, :session_user_history_index, :update, :delete, :authorizations, :test_drive_status, :liking_create, :liking_destroy, # likes :following_create, :following_show, :following_destroy, # followings @@ -1090,6 +1090,40 @@ class ApiUsersController < ApiController @teacher = User.find(params[:teacher_id]) end + def onboardings + @onboardings = current_user.onboarding_users.where('onboarding_status in (?)', [User::ONBOARDING_STATUS_ASSIGNED, User::ONBOARDING_STATUS_EMAILED]).order(:onboarder_assigned_at) + end + + def update_onboarding + user = User.find(params[:id]) + if params[:onboarding_lost_reason] + user.onboarding_lost_reason = params[:onboarding_lost_reason] + user.onboarding_lost_at = Date.today + end + if params[:onboarding_escalation_reason] + user.onboarding_escalation_reason = params[:onboarding_escalation_reason] + user.onboarding_escalated_at = Date.today + end + + user.onboarding_email_1_sent_at = Date.today if params[:onboarding_email_1_sent_at] + user.onboarding_email_2_sent_at = Date.today if params[:onboarding_email_2_sent_at] + user.onboarding_email_3_sent_at = Date.today if params[:onboarding_email_3_sent_at] + user.onboarding_email_4_sent_at = Date.today if params[:onboarding_email_4_sent_at] + user.onboarding_email_5_sent_at = Date.today if params[:onboarding_email_5_sent_at] + user.onboarding_test_session_scheduled_at = Date.today if params[:onboarding_test_session_scheduled_at] + user.onboarding_test_session_at = params[:onboarding_test_session_at] if params[:onboarding_test_session_at] + user.onboarding_test_session_outcome = params[:onboarding_test_session_outcome] if params[:onboarding_test_session_outcome] + user.onboarding_onboarded_at = Date.today if params[:onboarding_onboarded_at] + user.onboarding_onboarder_notes = params[:onboarding_onboarder_notes] if params[:onboarding_onboarder_notes] + user.save + user.reload + @onboarding = user + end + + def show_onboarding + @onboarding = User.find(params[:id]) + end + ###################### RECORDINGS ####################### # def recording_index # @recordings = User.recording_index(current_user, params[:id]) diff --git a/web/app/controllers/landings_controller.rb b/web/app/controllers/landings_controller.rb index b07fc3e04..91536c949 100644 --- a/web/app/controllers/landings_controller.rb +++ b/web/app/controllers/landings_controller.rb @@ -569,7 +569,7 @@ class LandingsController < ApplicationController body = "Name: #{@current_user.name}\n" body << "Email: #{@current_user.email}\n" - body << "Admin: #{@current_user.admin_student_url}\n" + body << "Admin: #{@current_user.admin_onboarding_url}\n" body << "Package Details: \n" body << " Package: #{card.lesson_package_type.id}\n" body << " Credits: #{card.credits}\n" diff --git a/web/app/views/api_users/onboardings.rabl b/web/app/views/api_users/onboardings.rabl new file mode 100644 index 000000000..83200aaea --- /dev/null +++ b/web/app/views/api_users/onboardings.rabl @@ -0,0 +1,3 @@ +object @onboardings + +extends "api_users/show_onboarding" diff --git a/web/app/views/api_users/show.rabl b/web/app/views/api_users/show.rabl index d077c0bea..79a2d99e3 100644 --- a/web/app/views/api_users/show.rabl +++ b/web/app/views/api_users/show.rabl @@ -1,7 +1,7 @@ object @user attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :online, :photo_url, :musician, :gender, :birth_date, :internet_service_provider, :friend_count, :liker_count, :like_count, :follower_count, :following_count, -:recording_count, :session_count, :biography, :favorite_count, :audio_latency, :upcoming_session_count, :age, :website, :skill_level, :reuse_card, :email_needs_verification, :is_a_teacher, :is_a_student +:recording_count, :session_count, :biography, :favorite_count, :audio_latency, :upcoming_session_count, :age, :website, :skill_level, :reuse_card, :email_needs_verification, :is_a_teacher, :is_a_student, :is_onboarder node :location do |user| if user.musician? diff --git a/web/app/views/api_users/show_onboarding.rabl b/web/app/views/api_users/show_onboarding.rabl new file mode 100644 index 000000000..85b73ad18 --- /dev/null +++ b/web/app/views/api_users/show_onboarding.rabl @@ -0,0 +1,3 @@ +object @onboarding + +attributes :id, :name, :email, :onboarding_status, :onboarder_assigned_at, :onboarding_email_1_sent_at, :onboarding_email_2_sent_at, :onboarding_email_3_sent_at, :onboarding_email_4_sent_at, :onboarding_email_5_sent_at, :onboarding_test_session_scheduled_at, :onboarding_test_session_outcome, :onboarding_test_session_at, :onboarding_onboarded_at, :onboarding_onboarder_notes, :onboarding_lost_reason, :onboarding_lost_at, :onboarding_escalation_reason, :onboarding_escalated_at diff --git a/web/app/views/api_users/update_onboarding.rabl b/web/app/views/api_users/update_onboarding.rabl new file mode 100644 index 000000000..cdb194a1a --- /dev/null +++ b/web/app/views/api_users/update_onboarding.rabl @@ -0,0 +1,3 @@ +object @onboarding + +extends "api_users/show_onboarding" diff --git a/web/app/views/clients/_account.html.erb b/web/app/views/clients/_account.html.erb index be2db2d64..71ce6cfb7 100644 --- a/web/app/views/clients/_account.html.erb +++ b/web/app/views/clients/_account.html.erb @@ -181,6 +181,22 @@
{% } %} + {% if (data.is_onboarder) { %} +
+
+

onboarding:

+
+
+
+ Manage the JamKazam users that you are assigned to for onboarding. +
+
+
+ UPDATE +
+
+ {% } %} + {% if (data.owns_school) { %}
diff --git a/web/app/views/clients/_account_onboarder.html.slim b/web/app/views/clients/_account_onboarder.html.slim new file mode 100644 index 000000000..c148c4b7b --- /dev/null +++ b/web/app/views/clients/_account_onboarder.html.slim @@ -0,0 +1,9 @@ +#account-onboarder.screen.secondary layout="screen" layout-id="account/onboarder" + .content-head + .content-icon + = image_tag "content/icon_account.png", :size => "27x20" + h1 + | jamclass + = render "screen_navigation" + .content-body + = react_component 'AccountOnboarderScreen', {} \ No newline at end of file diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index b32625196..8f86cd8c9 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -88,6 +88,7 @@ <%= render "account_payment_history" %> <%= render "account_school" %> <%= render "account_retailer" %> +<%= render "account_onboarder" %> <%= render "inviteMusicians" %> <%= render "hoverBand" %> <%= render "hoverFan" %> diff --git a/web/config/routes.rb b/web/config/routes.rb index 29c2c4019..131421f9d 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -462,6 +462,11 @@ Rails.application.routes.draw do match '/users/progression/social_promoted' => 'api_users#social_promoted', :via => :post match '/users/progression/opened_jamtrack_web_player' => 'api_users#opened_jamtrack_web_player', :via => :post + # onboarding + match '/users/:id/onboardings' => 'api_users#onboardings', :via => :get + match '/users/:id/onboardings' => 'api_users#update_onboarding', :via => :post + match '/users/:id/onboardings' => 'api_users#show_onboarding', :via => :get + # events match '/users/event/record' => 'api_users#user_event', :via => :post diff --git a/web/spec/features/book_test_drive_spec.rb b/web/spec/features/book_test_drive_spec.rb index 8b18ecd27..dda0582bd 100644 --- a/web/spec/features/book_test_drive_spec.rb +++ b/web/spec/features/book_test_drive_spec.rb @@ -240,5 +240,136 @@ describe "Test Drive", :js => true, :type => :feature, :capybara_feature => true teacher_distribution.distributed.should be false end + + it "same teacher with posa card succeeds" do + + PosaCard.activate(card_lessons, retailer) + card_lessons.reload + card_lessons.claim(user) + card_lessons.errors.any?.should be false + + + visit "/client#/teachers/search" + + #Timecop.travel(Date.new(2016, 04, 01)) + + find('.teacher-search-result[data-teacher-id="' + teacher_user.id + '"] .try-test-drive').trigger(:click) + + # no longer true + # TryTestDriveDialog shows + #find('.purchase-testdrive-now').trigger(:click) + + # dismiss banner + + fill_out_single_lesson(Time.now.to_date + 1, Time.now.to_date + 2) + + find('#banner h1', text: 'Lesson Requested') + find('a.button-orange', text:'CLOSE').trigger(:click) + + + # we tell user they have test drive purchased, and take them to the teacher screen + #find('#banner h1', text: 'TestDrive Purchased') + #find('#banner .dialog-inner', text: "You have purchased #{4} TestDrive credits and have used 1 credit to request a JamClass with #{teacher_user.name}") + + # dismiss banner + #find('a.button-orange', text:'CLOSE').trigger(:click) + + # validate that we made a test drive purchase + lesson_package_purchase = LessonPackagePurchase.where(user_id: user.id).first + lesson_package_purchase.should_not be_nil + lesson_package_purchase.lesson_package_type.is_test_drive?.should be true + lesson_package_purchase.posa_card.should eql card_lessons + lesson_package_purchase.lesson_payment_charge.should be_nil + user.reload + user.remaining_test_drives.should eql 0 + user.jamclass_credits.should eql 3 + #lesson_package_purchase.amount_charged.should eql 49.99 + user.sales.count.should eql 1 + sale = user.sales.first + sale.order_total.should eql 49.99 + sale.recurly_total_in_cents.should be_nil + + + + + + + user.reload + user.onboarding_status.should eql User::ONBOARDING_STATUS_UNASSIGNED + user.student_lesson_bookings.count.should eql 1 + lesson_booking = user.student_lesson_bookings.first + lesson_booking.is_requested?.should be true + user.remaining_test_drives.should eql 0 + lesson_booking.lesson_sessions.count.should eql 1 + + lesson_session1 = lesson_booking.lesson_sessions.first + # approve by teacher: + teacher_approve(lesson_session1) + successful_lesson(lesson_session1) + + # then log back in as student + switch_user(user, "/client#/teachers/search") + + + user.most_recent_test_drive_purchase.should_not be_nil + # let's make sure we can ask for another test drive too! + teacher_user.teacher.ready_for_session_at = Time.now + teacher_user.teacher.save! + + find('a.teacher-search-options').trigger(:click) + find('a.search-btn').trigger(:click) + + find('.teacher-search-result[data-teacher-id="' + teacher_user.id + '"] .try-test-drive').trigger(:click) + + find('h2', text: 'book testdrive lesson') + find('.booking-info', text: '3 TestDrive lesson credits') + + fill_out_single_lesson(Time.now.to_date + 1, Time.now.to_date + 2) + + # we tell user they have test drive purchased, and take them to the teacher screen + find('#banner h1', text: 'Lesson Requested') + # dismiss banner + find('a.button-orange', text:'CLOSE').trigger(:click) + + user.student_lesson_bookings.count.should eql 2 + lesson_booking2 = user.student_lesson_bookings.order(:created_at).last + lesson_booking2.teacher.should eql teacher_user + lesson_session2 = lesson_booking2.lesson_sessions[0] + lesson_session2.lesson_package_purchase.should_not be_nil + + # approve by teacher: + teacher_approve(lesson_session2) + + successful_lesson(lesson_session2) + + LessonSession.hourly_check + lesson_session1.reload + lesson_session1.analysed.should be true + analysis = lesson_session1.analysis + analysis["reason"].should eql LessonSessionAnalyser::SUCCESS + lesson_session1.billing_attempts.should be_nil + lesson_session1.billed.should eql false + lesson_session1.success.should be true + + + LessonBooking.hourly_check + + lesson_session1.reload + teacher_distribution = lesson_session1.teacher_distribution + teacher_distribution.amount_in_cents.should eql 1000 + teacher_distribution.ready.should be true + teacher_distribution.distributed.should be false + + lesson_session2.reload + teacher_distribution = lesson_session2.teacher_distribution + teacher_distribution.amount_in_cents.should eql 1000 + teacher_distribution.ready.should be true + teacher_distribution.distributed.should be false + + user.reload + user.first_onboarding_paid_lesson_at.should_not be_nil + user.onboarding_status.should eql User::ONBOARDING_STATUS_PAID_LESSON + + end end end diff --git a/web/spec/support/lessons.rb b/web/spec/support/lessons.rb index 962722889..93ddb3790 100644 --- a/web/spec/support/lessons.rb +++ b/web/spec/support/lessons.rb @@ -22,7 +22,8 @@ def failed_lesson(lesson_session, advance_to_end = true) end def teacher_approve(lesson_session) - sign_out_poltergeist(validate: true) + #sign_out_poltergeist(validate: true) + sign_out sign_in_poltergeist(lesson_session.teacher, password: 'foobar') visit "/client#/jamclass/lesson-booking/" + lesson_session.id find(".slot-decision-field[data-slot-id=\"#{lesson_session.lesson_booking.default_slot.id}\"] ins", visible: false).trigger(:click) @@ -35,21 +36,24 @@ end def date_picker_format(date) date.strftime('%a %b %d %Y') end -def fill_out_single_lesson +def fill_out_single_lesson(first_date = Date.new(2016, 4, 17), second_date = Date.new(2016, 4, 18)) + + first = first_date.strftime('%a %d %b %Y') + second = second_date.strftime('%a %d %b %Y') find('h2', text: 'book testdrive lesson') find('.booking-info', text: 'If you need to cancel') # book the lesson - fill_in "slot-1-date", with: "Sun Apr 17 2016" + fill_in "slot-1-date", with: first# "Sun Apr 17 2016" #find('.slot.slot-1 input.hasDatepicker').trigger(:click) # click 4-6 - find('td a', text: '17').trigger(:click) + find('td a', text: first_date.day).trigger(:click) #find('.slot.slot-2 input.hasDatepicker').trigger(:click) # click 4-7 - fill_in "slot-2-date", with: "Mon Apr 18 2016" - find('td a', text: '18').trigger(:click) + fill_in "slot-2-date", with: second #"Mon Apr 18 2016" + find('td a', text: second_date.day).trigger(:click) fill_in 'user-description', with: 'abc def dog neck'