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(`
| User | +Test Session | +Problems | +Onboarded | +Notes | +
|---|