From 5701e888a82a05de4a99aa18cd8f82972125a956 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 21 Apr 2016 09:23:29 -0500 Subject: [PATCH 01/36] Updating lessons with barely working version --- admin/app/admin/teachers.rb | 2 +- db/Gemfile.lock | 11 +- db/manifest | 3 +- db/up/lessons_unread_messages.sql | 34 ++ pb/src/client_container.proto | 2 + ruby/lib/jam_ruby.rb | 1 + ruby/lib/jam_ruby/app/mailers/user_mailer.rb | 73 +++ .../jam_ruby/user_mailer/lesson_chat.html.erb | 18 + .../jam_ruby/user_mailer/lesson_chat.text.erb | 7 + .../lesson_starting_soon_student.html.erb | 12 + .../lesson_starting_soon_student.text.erb | 4 + .../lesson_starting_soon_teacher.html.erb | 12 + .../lesson_starting_soon_teacher.text.erb | 3 + .../student_lesson_accepted.html.erb | 2 +- ...ent_scheduled_jamclass_invitation.html.erb | 2 +- .../student_welcome_message.html.erb | 2 +- .../teacher_lesson_accepted.html.erb | 2 +- ...her_scheduled_jamclass_invitation.html.erb | 2 +- ruby/lib/jam_ruby/message_factory.rb | 6 +- ruby/lib/jam_ruby/models/affiliate_partner.rb | 2 +- ruby/lib/jam_ruby/models/chat_message.rb | 42 +- ruby/lib/jam_ruby/models/lesson_booking.rb | 52 +- ruby/lib/jam_ruby/models/lesson_session.rb | 193 +++++-- .../models/lesson_session_analyser.rb | 22 +- ruby/lib/jam_ruby/models/music_session.rb | 31 +- ruby/lib/jam_ruby/models/notification.rb | 19 +- ruby/lib/jam_ruby/models/sale_line_item.rb | 5 +- ruby/lib/jam_ruby/models/signup_hint.rb | 19 +- ruby/lib/jam_ruby/models/teacher.rb | 9 +- .../jam_ruby/models/teacher_distribution.rb | 22 + ruby/lib/jam_ruby/models/user.rb | 31 +- .../jam_ruby/resque/scheduled/minutely_job.rb | 16 + .../flows/monthly_recurring_lesson_spec.rb | 9 +- .../spec/jam_ruby/flows/normal_lesson_spec.rb | 10 +- .../jam_ruby/flows/recurring_lesson_spec.rb | 7 +- .../jam_ruby/flows/testdrive_lesson_spec.rb | 6 +- .../jam_ruby/models/affiliate_partner_spec.rb | 16 + .../jam_ruby/models/lesson_booking_spec.rb | 19 +- .../jam_ruby/models/lesson_session_spec.rb | 22 +- .../models/teacher_distribution_spec.rb | 23 + ruby/spec/mailers/render_emails_spec.rb | 17 + web/Gemfile | 1 + .../images/content/icon_unread_mail.png | Bin 0 -> 1537 bytes .../accounts_payment_history_screen.js.coffee | 2 - web/app/assets/javascripts/dialog/banner.js | 5 +- web/app/assets/javascripts/globals.js | 3 +- web/app/assets/javascripts/jam_rest.js | 87 ++- web/app/assets/javascripts/jamkazam.js | 2 + .../jquery.lessonSessionActions.js | 91 ++++ web/app/assets/javascripts/layout.js | 8 +- .../assets/javascripts/notificationPanel.js | 58 +- .../AccountPaymentHistoryScreen.js.jsx.coffee | 382 ++++++++++++++ .../BookLessonFree.js.jsx.coffee | 18 +- .../CancelLessonDialog.js.jsx.coffee | 60 +++ .../react-components/ChatDialog.js.jsx.coffee | 77 +++ .../react-components/ChatWindow.js.jsx.coffee | 115 +++- .../JamClassScreen.js.jsx.coffee | 496 ++++++++++++++++++ .../JamClassStudentScreen.js.jsx.coffee | 154 ------ .../LessonBooking.js.jsx.coffee | 83 ++- .../LessonBookingDecision.js.jsx.coffee | 41 +- .../LessonPayment.js.jsx.coffee | 207 +++++--- .../RescheduleLessonDialog.js.jsx.coffee | 82 +++ .../StripeConnect.js.jsx.coffee | 18 +- .../TeacherSearchScreen.js.jsx.coffee | 25 +- .../TryTestDriveDialog.js.jsx.coffee | 4 +- .../actions/ChatActions.js.coffee | 1 + .../SchoolStudentLandingPage.js.jsx.coffee | 4 +- .../SchoolTeacherLandingPage.js.jsx.coffee | 4 +- .../mixins/ICheckMixin.js.coffee | 8 +- .../mixins/PostProcessorMixin.js.coffee | 59 +++ .../stores/ChatStore.js.coffee | 75 ++- .../stores/StripeStore.js.coffee | 16 +- .../client/accountPaymentHistory.css.scss | 95 +++- web/app/assets/stylesheets/client/client.css | 1 + .../stylesheets/client/jamkazam.css.scss | 8 + .../client/lessonSessionActions.css.scss | 40 ++ .../react-components/ChatWindow.css.scss | 121 ++++- .../react-components/JamClassScreen.css.scss | 190 +++++++ .../JamClassStudentScreen.css.scss | 86 --- .../LessonBookingScreen.css.scss | 6 + .../dialogs/CancelLessonDialog.css.scss | 19 + .../dialogs/RescheduleLessonDialog.css.scss | 19 + .../stylesheets/dialogs/chatDialog.css.scss | 22 + .../landings/school_landing.css.scss | 3 + web/app/controllers/api_chats_controller.rb | 15 +- .../api_lesson_bookings_controller.rb | 28 +- .../api_lesson_sessions_controller.rb | 82 ++- web/app/controllers/api_stripe_controller.rb | 1 - .../api_teacher_distributions_controller.rb | 17 + web/app/controllers/api_users_controller.rb | 2 +- web/app/controllers/sessions_controller.rb | 14 +- web/app/helpers/client_helper.rb | 2 +- web/app/helpers/music_session_helper.rb | 4 +- web/app/views/api_chats/show.rabl | 2 +- web/app/views/api_lesson_bookings/show.rabl | 19 +- web/app/views/api_lesson_sessions/show.rabl | 27 +- .../update_unread_messages.rabl | 3 + .../api_music_sessions/show_history.rabl | 1 + web/app/views/api_payment_histories/show.rabl | 2 +- web/app/views/api_stripe/store.rabl | 15 +- .../api_teacher_distributions/index.rabl | 11 + .../views/api_teacher_distributions/show.rabl | 4 + web/app/views/api_teachers/detail.rabl | 7 +- .../views/api_users/notification_index.rabl | 2 +- web/app/views/api_users/show.rabl | 4 +- .../_account_payment_history.html.slim | 21 +- .../clients/_lessonSessionActions.html.slim | 59 +++ web/app/views/clients/index.html.erb | 5 +- .../jamclass/_jamclass_student.html.slim | 2 +- .../jamclass/_lesson_payment.html.slim | 2 +- .../dialogs/_cancelLessonDialog.html.slim | 2 + web/app/views/dialogs/_chatDialog.html.slim | 2 + web/app/views/dialogs/_dialogs.html.haml | 5 +- .../dialogs/_rescheduleLessonDialog.html.slim | 2 + web/config/application.rb | 3 +- web/config/routes.rb | 13 +- web/config/scheduler.yml | 10 + web/spec/factories.rb | 12 + web/spec/features/book_single_lesson_spec.rb | 106 ++++ web/spec/features/book_test_drive_spec.rb | 103 ++++ web/spec/support/app_config.rb | 8 + 121 files changed, 3509 insertions(+), 694 deletions(-) create mode 100644 db/up/lessons_unread_messages.sql create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.text.erb create mode 100644 ruby/lib/jam_ruby/resque/scheduled/minutely_job.rb create mode 100644 ruby/spec/jam_ruby/models/teacher_distribution_spec.rb create mode 100644 web/app/assets/images/content/icon_unread_mail.png create mode 100644 web/app/assets/javascripts/jquery.lessonSessionActions.js create mode 100644 web/app/assets/javascripts/react-components/AccountPaymentHistoryScreen.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/CancelLessonDialog.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/ChatDialog.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/JamClassScreen.js.jsx.coffee delete mode 100644 web/app/assets/javascripts/react-components/JamClassStudentScreen.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/RescheduleLessonDialog.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/mixins/PostProcessorMixin.js.coffee create mode 100644 web/app/assets/stylesheets/client/lessonSessionActions.css.scss create mode 100644 web/app/assets/stylesheets/client/react-components/JamClassScreen.css.scss delete mode 100644 web/app/assets/stylesheets/client/react-components/JamClassStudentScreen.css.scss create mode 100644 web/app/assets/stylesheets/dialogs/CancelLessonDialog.css.scss create mode 100644 web/app/assets/stylesheets/dialogs/RescheduleLessonDialog.css.scss create mode 100644 web/app/assets/stylesheets/dialogs/chatDialog.css.scss create mode 100644 web/app/controllers/api_teacher_distributions_controller.rb create mode 100644 web/app/views/api_lesson_sessions/update_unread_messages.rabl create mode 100644 web/app/views/api_teacher_distributions/index.rabl create mode 100644 web/app/views/api_teacher_distributions/show.rabl create mode 100644 web/app/views/clients/_lessonSessionActions.html.slim create mode 100644 web/app/views/dialogs/_cancelLessonDialog.html.slim create mode 100644 web/app/views/dialogs/_chatDialog.html.slim create mode 100644 web/app/views/dialogs/_rescheduleLessonDialog.html.slim create mode 100644 web/spec/features/book_single_lesson_spec.rb create mode 100644 web/spec/features/book_test_drive_spec.rb diff --git a/admin/app/admin/teachers.rb b/admin/app/admin/teachers.rb index 942e501d5..e54718565 100644 --- a/admin/app/admin/teachers.rb +++ b/admin/app/admin/teachers.rb @@ -1,6 +1,6 @@ ActiveAdmin.register JamRuby::Teacher, :as => 'Teachers' do - menu :label => 'Teacher', :parent => 'JamClass' + menu :label => 'Teachers', :parent => 'JamClass' config.sort_order = 'created_at desc' config.batch_actions = false diff --git a/db/Gemfile.lock b/db/Gemfile.lock index 62739af59..649f3aaaf 100644 --- a/db/Gemfile.lock +++ b/db/Gemfile.lock @@ -1,18 +1,21 @@ GEM remote: http://rubygems.org/ specs: - little-plugger (1.1.3) + little-plugger (1.1.4) logging (1.7.2) little-plugger (>= 1.1.3) pg (0.17.1) - pg_migrate (0.1.13) + pg_migrate (0.1.14) logging (= 1.7.2) pg (= 0.17.1) thor - thor (0.18.1) + thor (0.19.1) PLATFORMS ruby DEPENDENCIES - pg_migrate (= 0.1.13)! + pg_migrate (= 0.1.14)! + +BUNDLED WITH + 1.11.2 diff --git a/db/manifest b/db/manifest index 4067937f3..b722f957d 100755 --- a/db/manifest +++ b/db/manifest @@ -341,4 +341,5 @@ email_blacklist.sql jamblaster_connection.sql teacher_progression.sql teacher_complete.sql -lessons.sql \ No newline at end of file +lessons.sql +lessons_unread_messages.sql \ No newline at end of file diff --git a/db/up/lessons_unread_messages.sql b/db/up/lessons_unread_messages.sql new file mode 100644 index 000000000..1b2d65f26 --- /dev/null +++ b/db/up/lessons_unread_messages.sql @@ -0,0 +1,34 @@ +ALTER TABLE chat_messages DROP COLUMN lesson_booking_id; +ALTER TABLE chat_messages ADD COLUMN lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id); +ALTER TABLE lesson_sessions ADD COLUMN teacher_unread_messages BOOLEAN DEFAULT FALSE NOT NULL; +ALTER TABLE lesson_sessions ADD COLUMN student_unread_messages BOOLEAN DEFAULT FALSE NOT NULL; +ALTER TABLE chat_messages ADD COLUMN purpose VARCHAR(200); +ALTER TABLE lesson_sessions ADD COLUMN student_short_canceled BOOLEAN DEFAULT FALSE NOT NULL; +ALTER TABLE lesson_sessions ADD COLUMN teacher_short_canceled BOOLEAN DEFAULT FALSE NOT NULL; +ALTER TABLE lesson_sessions ADD COLUMN sent_starting_notice BOOLEAN DEFAULT FALSE NOT NULL; + +ALTER TABLE lesson_bookings DROP CONSTRAINT lesson_bookings_counter_slot_id_fkey; +ALTER TABLE lesson_bookings ADD CONSTRAINT lesson_bookings_counter_slot_id_fkey FOREIGN KEY (counter_slot_id) REFERENCES lesson_booking_slots(id) ON DELETE CASCADE; + +ALTER TABLE lesson_bookings DROP CONSTRAINT lesson_bookings_default_slot_id_fkey; +ALTER TABLE lesson_bookings ADD CONSTRAINT lesson_bookings_default_slot_id_fkey FOREIGN KEY (default_slot_id) REFERENCES lesson_booking_slots(id) ON DELETE CASCADE; + + +ALTER TABLE lesson_sessions DROP CONSTRAINT lesson_sessions_slot_id_fkey; +ALTER TABLE lesson_sessions ADD CONSTRAINT lesson_sessions_slot_id_fkey FOREIGN KEY (slot_id) REFERENCES lesson_booking_slots(id) ON DELETE CASCADE; + +ALTER TABLE users DROP CONSTRAINT users_teacher_id_fkey; +ALTER TABLE users ADD CONSTRAINT users_teacher_id_fkey FOREIGN KEY (teacher_id) REFERENCES teachers(id) ON DELETE CASCADE; + +ALTER TABLE music_sessions DROP CONSTRAINT music_sessions_lesson_session_id_fkey; +ALTER TABLE music_sessions ADD CONSTRAINT music_sessions_lesson_session_id_fkey FOREIGN KEY (lesson_session_id) REFERENCES lesson_sessions(id) ON DELETE SET NULL; + +ALTER TABLE notifications DROP CONSTRAINT notifications_lesson_session_id_fkey; +ALTER TABLE notifications ADD CONSTRAINT notifications_lesson_session_id_fkey FOREIGN KEY (lesson_session_id) REFERENCES lesson_sessions(id) ON DELETE CASCADE; + +ALTER TABLE chat_messages DROP CONSTRAINT chat_messages_lesson_session_id_fkey; +ALTER TABLE chat_messages ADD CONSTRAINT chat_messages_lesson_session_id_fkey FOREIGN KEY (lesson_session_id) REFERENCES lesson_sessions(id) ON DELETE CASCADE; + +ALTER TABLE chat_messages DROP CONSTRAINT chat_messages_target_user_id_fkey; +ALTER TABLE chat_messages ADD CONSTRAINT chat_messages_target_user_id_fkey FOREIGN KEY (lesson_session_id) REFERENCES lesson_sessions(id) ON DELETE SET NULL; + diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index 57799fc7b..207d61e5d 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -619,6 +619,8 @@ message ChatMessage { optional string msg_id = 4; optional string created_at = 5; optional string channel = 6; + optional string lesson_session_id = 7; + optional string purpose = 8; } message SendChatMessage { diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 2fddb85f5..e2e86525e 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -57,6 +57,7 @@ require "jam_ruby/resque/scheduled/unused_music_notation_cleaner" require "jam_ruby/resque/scheduled/user_progress_emailer" require "jam_ruby/resque/scheduled/daily_job" require "jam_ruby/resque/scheduled/hourly_job" +require "jam_ruby/resque/scheduled/minutely_job" require "jam_ruby/resque/scheduled/daily_session_emailer" require "jam_ruby/resque/scheduled/new_musician_emailer" require "jam_ruby/resque/scheduled/music_session_reminder" diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb index db0933054..d6dd849ca 100644 --- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb @@ -1485,5 +1485,78 @@ module JamRuby format.html end end + + def lesson_chat(chat_msg) + @target = chat_msg.target_user + @sender = chat_msg.user + @message = chat_msg.message + @lesson_session = chat_msg.lesson_session + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) + + email = @target.email + @subject = "#{@sender.name} has sent you a message about a lesson" + unique_args = {:type => "lesson_chat"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@target.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def lesson_starting_soon_teacher(lesson_session) + @lesson_booking = lesson_booking = lesson_session.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @lesson_session = lesson_booking.next_lesson + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) + + email = @teacher.email + @subject = "Your lesson with #{@student.first_name} on JamKazam is starting soon" + unique_args = {:type => "send_starting_notice_teacher"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@teacher.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + + def lesson_starting_soon_student(lesson_session) + @lesson_booking = lesson_booking = lesson_session.lesson_booking + @student = lesson_booking.student + @teacher = lesson_booking.teacher + @message = message + @lesson_session = lesson_booking.next_lesson + @session_name = @lesson_session.music_session.name + @session_description = @lesson_session.music_session.description + @session_date = @lesson_session.slot.pretty_scheduled_start(true) + + email = @student.email + @subject = "Your lesson with #{@student.first_name} on JamKazam is starting soon" + unique_args = {:type => "send_starting_notice_student"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => @subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end end end diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.html.erb new file mode 100644 index 000000000..4b11f3cc0 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.html.erb @@ -0,0 +1,18 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @sender.resolved_photo_url) %> + +<% content_for :note do %> +

+ + <% if @message.present? %> +

<%= @sender.name %> says: +
<%= @message %> +
+ <% end %> +

Click the button below to view the entire lesson conversation.

+

+ VIEW LESSON CONVERSATION +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.text.erb new file mode 100644 index 000000000..43b683250 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.text.erb @@ -0,0 +1,7 @@ +<%= @subject %> + +<%= @sender.name %> says: + +<%= @message %> + +To see the full lesson conversation, click here: <%= @lesson_session.chat_url %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.html.erb new file mode 100644 index 000000000..d4744ff94 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.html.erb @@ -0,0 +1,12 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Your lesson with <%= @teacher.name %> is scheduled to begin on JamKazam in less than 30 minutes. +

+ JAMCLASS HOME +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.text.erb new file mode 100644 index 000000000..f880fcb55 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_student.text.erb @@ -0,0 +1,4 @@ +Your lesson with <%= @teacher.name %> is scheduled to begin on JamKazam in less than 30 minutes. + +JAMCLASS HOME (<%= @lesson_session.home_url %> + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.html.erb new file mode 100644 index 000000000..96ecf3c06 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.html.erb @@ -0,0 +1,12 @@ +<% provide(:title, @subject) %> +<% provide(:photo_url, @student.resolved_photo_url) %> + +<% content_for :note do %> +

+ Your lesson with <%= @student.name %> is scheduled to begin on JamKazam in less than 30 minutes. +

+ JAMCLASS HOME +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.text.erb new file mode 100644 index 000000000..c8f460e25 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.text.erb @@ -0,0 +1,3 @@ +Your lesson with <%= @student.name %> is scheduled to begin on JamKazam in less than 30 minutes. +JAMCLASS HOME (<%= @lesson_session.home_url %> + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb index d60afb1dc..a5b2c826a 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb @@ -14,7 +14,7 @@
<%= @message %>
<% end %> -

Click the button below to get more information and to add this lesson to your calendar! +

We strongly suggest adding this to your calendar so you don't forget it, or you'll end up paying for a lesson you don't get.

VIEW LESSON DETAILS diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb index 67590256e..f192b7b9c 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb @@ -8,4 +8,4 @@ <%= @session_date %>

-

VIEW LESSON DETAILS

\ No newline at end of file +

VIEW LESSON DETAILS

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb index 88aacda6e..574aa3885 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb @@ -32,7 +32,7 @@ teacher for you. Finding the right teacher is the single most important determinant of success in your lessons. Would you marry the first person you ever dated? No? Same here. Pick 4 teachers who look great, and then see who you click with. It's a phenomenal value, and then you can stick with the best teacher for you. - Click this link + Click this link to sign up now for TestDrive. Then you can book 4 TestDrive lessons to get rolling.

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb index 7e76b8c0c..ab838c208 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb @@ -8,7 +8,7 @@ <% else %> This student has accepted your lesson request! <% end %> -

Click the button below to get more information and to add this lesson to your calendar! +

We strongly suggest adding this to your calendar so you don't forget it.

VIEW LESSON DETAILS diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb index 67590256e..f192b7b9c 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb @@ -8,4 +8,4 @@ <%= @session_date %>

-

VIEW LESSON DETAILS

\ No newline at end of file +

VIEW LESSON DETAILS

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index 30763c028..53e63e9e3 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -993,14 +993,16 @@ module JamRuby end # creates the chat message - def chat_message(session_id, sender_name, sender_id, msg, msg_id, created_at, channel) + def chat_message(session_id, sender_name, sender_id, msg, msg_id, created_at, channel, lesson_session_id, purpose) chat_message = Jampb::ChatMessage.new( :sender_id => sender_id, :sender_name => sender_name, :msg => msg, :msg_id => msg_id, :created_at => created_at, - :channel => channel + :channel => channel, + :lesson_session_id => lesson_session_id, + :purpose => purpose ) if session_id diff --git a/ruby/lib/jam_ruby/models/affiliate_partner.rb b/ruby/lib/jam_ruby/models/affiliate_partner.rb index aaf05fc35..0461490f7 100644 --- a/ruby/lib/jam_ruby/models/affiliate_partner.rb +++ b/ruby/lib/jam_ruby/models/affiliate_partner.rb @@ -357,7 +357,7 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base UPDATE affiliate_quarterly_payments SET closed = TRUE, closed_at = NOW() - WHERE year < #{year} OR quarter < #{quarter} + WHERE year < #{year} OR (year = #{year} AND quarter < #{quarter}) } ActiveRecord::Base.connection.execute(sql) diff --git a/ruby/lib/jam_ruby/models/chat_message.rb b/ruby/lib/jam_ruby/models/chat_message.rb index 3c6b056db..7c14dbd84 100644 --- a/ruby/lib/jam_ruby/models/chat_message.rb +++ b/ruby/lib/jam_ruby/models/chat_message.rb @@ -16,26 +16,44 @@ module JamRuby belongs_to :user belongs_to :music_session belongs_to :target_user, class_name: "JamRuby::User" - belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" + belongs_to :lesson_session, class_name: "JamRuby::LessonSession" validates :user, presence: true validates :message, length: {minimum: 1, maximum: 255}, no_profanity: true, unless: :ignore_message_checks - def self.create(user, music_session, message, channel, client_id, target_user = nil, lesson_booking = nil) + def self.create(user, music_session, message, channel, client_id, target_user = nil, lesson_session = nil, purpose = nil) chat_msg = ChatMessage.new chat_msg.user_id = user.id chat_msg.music_session_id = music_session.id if music_session chat_msg.message = message chat_msg.channel = channel chat_msg.target_user = target_user - chat_msg.lesson_booking = lesson_booking + chat_msg.lesson_session = lesson_session + chat_msg.purpose = purpose - if lesson_booking + + if lesson_session chat_msg.ignore_message_checks = true + + if user.id == lesson_session.student.id + lesson_session.teacher_unread_messages = true + Notification.send_lesson_message('chat', lesson_session, false, message) + else + lesson_session.student_unread_messages = true + Notification.send_lesson_message('chat', lesson_session, true, message) + end + + lesson_session.save(validate: false) + + # a nil purpose means 'normal chat', which is the only time we should send an email + if !target_user.online? && purpose.nil? && message.present? + UserMailer.lesson_chat(chat_msg).deliver! + + end end if chat_msg.save - ChatMessage.send_chat_msg music_session, chat_msg, user, client_id, channel + ChatMessage.send_chat_msg music_session, chat_msg, user, client_id, channel, lesson_session, purpose, target_user end chat_msg end @@ -60,6 +78,11 @@ module JamRuby query = ChatMessage.where('music_session_id = ?', music_session_id) end + if params.has_key? (:lesson_session) + lesson_session_id = params[:lesson_session] + query = ChatMessage.where('lesson_session_id = ?', lesson_session_id) + end + query = query.offset(start).limit(limit).order('created_at DESC').includes([:user]) if query.length == 0 @@ -71,7 +94,7 @@ module JamRuby end end - def send_chat_msg(music_session, chat_msg, user, client_id, channel) + def send_chat_msg(music_session, chat_msg, user, client_id, channel, lesson_session, purpose, target_user) music_session_id = music_session.id if music_session msg = @@message_factory.chat_message( @@ -81,14 +104,19 @@ module JamRuby chat_msg.message, chat_msg.id, chat_msg.created_at.utc.iso8601, - channel + channel, + lesson_session.id, + purpose ) if channel == 'session' @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id}) elsif channel == 'global' @@mq_router.publish_to_active_clients(msg) + elsif channel == 'lesson' + @@mq_router.publish_to_user(target_user.id, msg, sender = {:client_id => client_id}) end + end end end diff --git a/ruby/lib/jam_ruby/models/lesson_booking.rb b/ruby/lib/jam_ruby/models/lesson_booking.rb index 384009aaa..4d7be82b9 100644 --- a/ruby/lib/jam_ruby/models/lesson_booking.rb +++ b/ruby/lib/jam_ruby/models/lesson_booking.rb @@ -9,7 +9,7 @@ module JamRuby @@log = Logging.logger[LessonBooking] - attr_accessor :accepting, :countering, :countered_slot, :countered_lesson + attr_accessor :accepting, :countering, :canceling, :countered_slot, :countered_lesson STATUS_REQUESTED = 'requested' STATUS_CANCELED = 'canceled' @@ -37,12 +37,11 @@ module JamRuby belongs_to :teacher, class_name: "JamRuby::User" belongs_to :accepter, class_name: "JamRuby::User" belongs_to :canceler, class_name: "JamRuby::User" - belongs_to :default_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :default_slot_id, inverse_of: :defaulted_booking - belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_booking - - has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot" - has_many :lesson_sessions, class_name: "JamRuby::LessonSession" - has_many :lesson_package_purchases, class_name: "JamRuby::LessonPackagePurchase" + belongs_to :default_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :default_slot_id, inverse_of: :defaulted_booking, :dependent => :destroy + belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_booking, :dependent => :destroy + has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot", :dependent => :delete_all + has_many :lesson_sessions, class_name: "JamRuby::LessonSession", :dependent => :destroy + has_many :lesson_package_purchases, class_name: "JamRuby::LessonPackagePurchase", :dependent => :destroy validates :user, presence: true validates :teacher, presence: true @@ -63,6 +62,8 @@ module JamRuby validate :validate_lesson_length validate :validate_payment_style validate :validate_accepted, :if => :accepting + validate :validate_canceled, :if => :canceling + before_save :before_save @@ -136,6 +137,9 @@ module JamRuby def next_lesson if recurring session = lesson_sessions.joins(:music_session).where("scheduled_start is not null").where("scheduled_start > ?", Time.now).order(:created_at).first + if session.nil? + session = lesson_sessions[0] + end LessonSession.find(session.id) if session else lesson_sessions[0] @@ -390,6 +394,14 @@ module JamRuby self.accepting = false end + def validate_canceled + if !is_canceled? + self.errors.add(:status, "This session is already #{self.status}.") + end + + self.canceling = false + end + def send_notices UserMailer.student_lesson_request(self).deliver UserMailer.teacher_lesson_request(self).deliver @@ -500,41 +512,49 @@ module JamRuby def approved_before? !self.accepter_id.nil? end + def cancel(canceler, other, message) + self.canceling = true self.active = false self.status = STATUS_CANCELED self.cancel_message = message self.canceler = canceler success = save if success + lesson_sessions.past_cancel_window.each do |lesson_session| + lesson_session = LessonSession.find(lesson_session.id) # because .upcoming creates ReadOnly records + lesson_session.cancel_lesson(canceler, message) + if !lesson_session.save + return lesson_session + end + end if approved_before? # just tell both people it's cancelled, to act as confirmation Notification.send_lesson_message('canceled', next_lesson, false) Notification.send_lesson_message('canceled', next_lesson, true) UserMailer.student_lesson_booking_canceled(self, message).deliver UserMailer.teacher_lesson_booking_canceled(self, message).deliver - chat_message_prefix = "Lesson Canceled" + purpose = "Lesson Canceled" else if canceler == student # if it's the first time acceptance student canceling, we call it a 'cancel' Notification.send_lesson_message('canceled', next_lesson, false) UserMailer.teacher_lesson_booking_canceled(self, message).deliver - chat_message_prefix = "Lesson Canceled" + purpose = "Lesson Canceled" else # if it's the first time acceptance teacher, it was declined UserMailer.student_lesson_booking_declined(self, message).deliver Notification.send_lesson_message('declined', next_lesson, true) - chat_message_prefix = "Lesson Declined" + purpose = "Lesson Declined" end end - chat_message = message.nil? ? chat_message_prefix : "#{chat_message_prefix}: #{message}" - msg = ChatMessage.create(canceler, nil, chat_message, ChatMessage::CHANNEL_LESSON, nil, other, self) - + message = '' if message.nil? + msg = ChatMessage.create(canceler, nil, message, ChatMessage::CHANNEL_LESSON, nil, other, next_lesson, purpose) end - success + self end def card_approved @@ -641,7 +661,8 @@ module JamRuby end if lesson_booking_slots if lesson_booking.save - msg = ChatMessage.create(user, lesson_booking.lesson_sessions[0], description, ChatMessage::CHANNEL_LESSON, nil, teacher, lesson_booking) + description = '' if description.nil? + msg = ChatMessage.create(user, lesson_booking.lesson_sessions[0], description, ChatMessage::CHANNEL_LESSON, nil, teacher, lesson_booking.lesson_sessions[0], 'Lesson Requested') end end lesson_booking @@ -806,7 +827,6 @@ module JamRuby end end - def home_url APP_CONFIG.external_root_url + "/client#/jamclass" end diff --git a/ruby/lib/jam_ruby/models/lesson_session.rb b/ruby/lib/jam_ruby/models/lesson_session.rb index dec9512fd..28d27b90a 100644 --- a/ruby/lib/jam_ruby/models/lesson_session.rb +++ b/ruby/lib/jam_ruby/models/lesson_session.rb @@ -10,8 +10,8 @@ module JamRuby @@log = Logging.logger[LessonSession] - delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed, :billed_at, :billing_error_detail, :billing_error_reason, :is_card_declined?, :is_card_expired?, :last_billed_at_date, :sent_billing_notices, to: :lesson_payment_charge - delegate :is_test_drive?, :is_single_free?, :is_normal?, :approved_before?, :is_active?, to: :lesson_booking + delegate :sent_billing_notices, :last_billing_attempt_at, :billing_attempts, :billing_should_retry, :billed_at, :billing_error_detail, :billing_error_reason, :is_card_declined?, :is_card_expired?, :last_billed_at_date, :sent_billing_notices, to: :lesson_payment_charge, allow_nil: true + delegate :is_test_drive?, :is_single_free?, :is_normal?, :approved_before?, :is_active?, :recurring, to: :lesson_booking delegate :pretty_scheduled_start, to: :music_session @@ -30,16 +30,19 @@ module JamRuby LESSON_TYPE_TEST_DRIVE = 'test-drive' LESSON_TYPES = [LESSON_TYPE_SINGLE, LESSON_TYPE_SINGLE_FREE, LESSON_TYPE_TEST_DRIVE] - has_one :music_session, class_name: "JamRuby::MusicSession" + has_one :music_session, class_name: "JamRuby::MusicSession", :dependent => :destroy belongs_to :teacher, class_name: "JamRuby::User", foreign_key: :teacher_id, inverse_of: :taught_lessons belongs_to :canceler, class_name: "JamRuby::User", foreign_key: :canceler_id belongs_to :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase" belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking" - belongs_to :slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :slot_id + belongs_to :slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :slot_id, :dependent => :destroy belongs_to :lesson_payment_charge, class_name: "JamRuby::LessonPaymentCharge", foreign_key: :charge_id - belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_lesson + belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_lesson, :dependent => :destroy has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution" has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot" + has_many :notifications, :class_name => "JamRuby::Notification", :foreign_key => "lesson_session_id" + has_many :chat_messages, :class_name => "JamRuby::ChatMessage", :foreign_key => "lesson_session_id" + validates :duration, presence: true, numericality: {only_integer: true} @@ -69,6 +72,8 @@ module JamRuby scope :suspended, -> { where(status: STATUS_SUSPENDED) } scope :completed, -> { where(status: STATUS_COMPLETED) } scope :missed, -> { where(status: STATUS_MISSED) } + scope :upcoming, -> { joins(:music_session).where('music_sessions.scheduled_start > ?', Time.now) } + scope :past_cancel_window, -> { joins(:music_session).where('music_sessions.scheduled_start > ?', 24.hours.from_now ) } def create_charge if !is_test_drive? @@ -96,23 +101,51 @@ module JamRuby self.save end + def music_session_id + music_session.id + end def self.hourly_check analyse_sessions complete_sessions end + def self.minutely_check + upcoming_sessions_reminder + end + def self.analyse_sessions - MusicSession.joins(lesson_session: :lesson_booking).where('session_removed_at IS NOT NULL').where('analysed = false').each do |music_session| + MusicSession.joins(lesson_session: :lesson_booking).where("session_removed_at IS NOT NULL OR NOW() > scheduled_start + (INTERVAL '1 minutes' * duration)").where('analysed = false').each do |music_session| lession_session = music_session.lesson_session lession_session.analyse end end def self.complete_sessions - MusicSession.joins(lesson_session: [:lesson_booking, :lesson_payment_charge]).where('session_removed_at IS NOT NULL').where('analysed = true').where('lesson_sessions.post_processed = false').where('billing_should_retry = true').each do |music_session| + # this will find any paid session (recurring monthly paid, recurring single paid, single paid) + MusicSession.joins(lesson_session: [:lesson_booking, :lesson_payment_charge]).where("session_removed_at IS NOT NULL OR NOW() > scheduled_start + (INTERVAL '1 minutes' * duration)").where('analysed = true').where('lesson_sessions.post_processed = false').where('billing_should_retry = true').each do |music_session| lession_session = music_session.lesson_session lession_session.session_completed end + + # test drives don't have a lesson_payment_charge, so we don't join against them + MusicSession.joins(lesson_session: [:lesson_booking]).where('lesson_sessions.lesson_type = ?', LESSON_TYPE_TEST_DRIVE).where("session_removed_at IS NOT NULL OR NOW() > scheduled_start + (INTERVAL '1 minutes' * duration)").where('analysed = true').where('lesson_sessions.post_processed = false').each do |music_session| + lession_session = music_session.lesson_session + lession_session.session_completed + end + end + + def self.upcoming_sessions_reminder + now = Time.now + half_hour_from_now = 30.minutes.from_now + if Time.zone + now = Time.zone.local_to_utc(now) + half_hour_from_now = Time.zone.local_to_utc(half_hour_from_now) + end + + MusicSession.joins(lesson_session: [:lesson_booking]).where('sent_starting_notice = false').where('(scheduled_start > ? and scheduled_start < ?)', now, half_hour_from_now).each do |music_session| + lession_session = music_session.lesson_session + lession_session.send_starting_notice + end end def analyse @@ -127,6 +160,8 @@ module JamRuby self.analysed_at = Time.now self.analysed = true + self.status = STATUS_COMPLETED + if lesson_booking.requires_teacher_distribution?(self) self.teacher_distribution = TeacherDistribution.create_for_lesson(self) end @@ -137,6 +172,13 @@ module JamRuby end end + def billed + if lesson_booking.is_test_drive? + false + else + lesson_payment_charge.billed + end + end def amount_charged lesson_payment_charge.amount_in_cents / 100.0 @@ -163,6 +205,13 @@ module JamRuby json.to_json end + def send_starting_notice + UserMailer.lesson_starting_soon_student(self).deliver! + UserMailer.lesson_starting_soon_teacher(self).deliver! + + self.sent_starting_notice = true + self.save(validate: false) + end def session_completed LessonSession.transaction do self.lock! @@ -273,7 +322,7 @@ module JamRuby else if !sent_notices UserMailer.student_lesson_normal_no_bill(self).deliver - UserMailer.teacher_lesson_no_bill(self).deliver + UserMailer.teacher_lesson_normal_no_bill(self).deliver self.sent_notices = true self.sent_notices_at = Time.now self.post_processed = true @@ -314,10 +363,6 @@ module JamRuby status == STATUS_COMPLETED end - def is_missed? - status == STATUS_MISSED - end - def is_approved? status == STATUS_APPROVED end @@ -330,6 +375,10 @@ module JamRuby status == STATUS_COUNTERED end + def analysis_json + @parsed_analysis || analysis ? JSON.parse(analysis) : nil + end + def validate_creating if !is_requested? && !is_approved? self.errors.add(:status, "is not valid for a new lesson session.") @@ -398,16 +447,22 @@ module JamRuby limit ||= 100 limit = limit.to_i - query = LessonSession.joins(:music_session).joins(music_session: :creator) - query = query.includes([:teacher, :music_session]) + query = LessonSession.unscoped.joins([:music_session, :lesson_booking]).joins(music_session: :creator) + #query = query.includes([:teacher, :music_session]) + query = query.includes([:music_session]) query = query.order('music_sessions.scheduled_start DESC') - if params[:as_teacher] - query = query.where('lesson_sessions.teacher_id = ?', user.id) + if params[:as_teacher].present? + if params[:as_teacher] + query = query.where('lesson_sessions.teacher_id = ?', user.id) + else + query = query.where('music_sessions.user_id = ?', user.id) + end else - query = query.where('music_sessions.user_id = ?', user.id) + query = query.where('(lesson_sessions.teacher_id = ? or music_sessions.user_id = ?)', user.id, user.id) end + query = query.where('lesson_bookings.card_presumed_ok = true OR (music_sessions.user_id = ?)', user.id) current_page = params[:page].nil? ? 1 : params[:page].to_i next_page = current_page + 1 @@ -451,6 +506,17 @@ module JamRuby time.nil? ? nil : attempt end + def school_owner_id + school = teacher.teacher.school + if school + school.owner.id + end + end + + def access?(user) + user.id == music_session.user_id || user.id == teacher.id || user.id == school_owner_id + end + # teacher accepts the lesson def accept(params) response = self @@ -480,8 +546,8 @@ module JamRuby end UserMailer.student_lesson_accepted(self, message, slot).deliver UserMailer.teacher_lesson_accepted(self, message, slot).deliver - chat_message = message ? "Lesson Approved: '#{message}'" : "Lesson Approved" - msg = ChatMessage.create(teacher, nil, chat_message, ChatMessage::CHANNEL_LESSON, nil, student, lesson_booking) + message = '' if message.nil? + msg = ChatMessage.create(teacher, nil, message, ChatMessage::CHANNEL_LESSON, nil, student, self, "Lesson Approved") Notification.send_jamclass_invitation_teacher(music_session, teacher) Notification.send_student_jamclass_invitation(music_session, student) Notification.send_lesson_message('accept', self, true) @@ -501,8 +567,8 @@ module JamRuby response = lesson_booking raise ActiveRecord::Rollback end - chat_message = message ? "All Lesson Times Updated: '#{message}'" : "All Lesson Times Updated" - msg = ChatMessage.create(slot.proposer, nil, chat_message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, lesson_booking) + message = '' if message.nil? + msg = ChatMessage.create(slot.proposer, nil, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, self, "All Lesson Times Updated") Notification.send_lesson_message('accept', self, true) # TODO: this isn't quite an 'accept' UserMailer.student_lesson_update_all(self, message, slot).deliver UserMailer.teacher_lesson_update_all(self, message, slot).deliver @@ -515,8 +581,8 @@ module JamRuby puts("unable to accept slot #{slot.id} for lesson #{self.id} because it's in the past") raise ActiveRecord::Rollback end - chat_message = message ? "Lesson Updated Time Approved: '#{message}'" : "Lesson Updated Time Approved" - msg = ChatMessage.create(slot.proposer, nil, chat_message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, lesson_booking) + message = '' if message.nil? + msg = ChatMessage.create(slot.proposer, nil, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, self, "Lesson Updated Time Approved") UserMailer.student_lesson_accepted(self, message, slot).deliver UserMailer.teacher_lesson_accepted(self, message, slot).deliver end @@ -551,25 +617,42 @@ module JamRuby self.counter_slot = slot end if self.save - if update_all - if !lesson_booking.counter(self, proposer, slot) - response = lesson_booking - raise ActiveRecord::Rollback - end + if update_all && !lesson_booking.counter(self, proposer, slot) + response = lesson_booking + raise ActiveRecord::Rollback end else response = self raise ActiveRecord::Rollback end - - msg = ChatMessage.create(slot.proposer, music_session, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, lesson_booking) + message = '' if message.nil? + msg = ChatMessage.create(slot.proposer, music_session, message, ChatMessage::CHANNEL_LESSON, nil, slot.recipient, self, "New Time Proposed") Notification.send_lesson_message('counter', self, slot.is_teacher_created?) end response end + def cancel_lesson(canceler, message) + canceled_by_student = canceler == student + self.status = STATUS_CANCELED + self.cancel_message = message + self.canceler = canceler + self.canceling = true + + if canceled_by_student + self.student_canceled = true + self.student_canceled_at = Time.now + self.student_canceled_reason = message + self.student_short_canceled = 24.hours.from_now > scheduled_start + else + self.teacher_canceled = true + self.teacher_canceled_at = Time.now + self.teacher_canceled_reason = message + self.teacher_short_canceled = 24.hours.from_now > scheduled_start + end + end # teacher accepts the lesson def cancel(params) @@ -577,39 +660,39 @@ module JamRuby LessonSession.transaction do canceler = params[:canceler] - other = canceler == teacher ? student : teacher + canceled_by_student = canceler == student + other = canceled_by_student ? teacher : student message = params[:message] + message = '' if message.nil? - if params[:update_all].present? + if lesson_booking.recurring update_all = params[:update_all] else - update_all = !lesson_booking.recurring + update_all = true end + if lesson_booking.is_test_drive? + student.test_drive_declined(self) + end - self.status = STATUS_CANCELED - self.cancel_message = message - self.canceler = canceler - self.canceling = true - - if self.save - if update_all - if !lesson_booking.cancel(canceler, other, message) - response = lesson_booking - raise ActiveRecord::Rollback - end - else - msg = ChatMessage.create(canceler, nil, message, ChatMessage::CHANNEL_LESSON, nil, other, lesson_booking) - Notification.send_lesson_message('canceled', self, false) - Notification.send_lesson_message('canceled', self, true) - UserMailer.student_lesson_canceled(self, message).deliver - UserMailer.teacher_lesson_canceled(self, message).deliver + if update_all + response = lesson_booking.cancel(canceler, other, message) + if response.errors.any? + raise ActiveRecord::Rollback end else - response = self - raise ActiveRecord::Rollback - end + cancel_lesson(canceler, message) + if !save + response = self + raise ActiveRecord::Rollback + end + msg = ChatMessage.create(canceler, nil, message, ChatMessage::CHANNEL_LESSON, nil, other, self, "Lesson Canceled") + Notification.send_lesson_message('canceled', self, false) + Notification.send_lesson_message('canceled', self, true) + UserMailer.student_lesson_canceled(self, message).deliver + UserMailer.teacher_lesson_canceled(self, message).deliver + end end response @@ -638,5 +721,9 @@ module JamRuby def admin_url APP_CONFIG.admin_root_url + "/admin/lesson_sessions/" + id end + + def chat_url + APP_CONFIG.external_root_url + "/client#/jamclass/chat-dialog/d1=lesson_" + id + end end end diff --git a/ruby/lib/jam_ruby/models/lesson_session_analyser.rb b/ruby/lib/jam_ruby/models/lesson_session_analyser.rb index d1883a77d..0526ce385 100644 --- a/ruby/lib/jam_ruby/models/lesson_session_analyser.rb +++ b/ruby/lib/jam_ruby/models/lesson_session_analyser.rb @@ -1,7 +1,7 @@ module JamRuby class LessonSessionAnalyser - SUCCESS = 'success' + SUCCESS = 'success' SESSION_ONGOING = 'session_ongoing' THRESHOLD_MET = 'threshold_met' WAITED_CORRECTLY = 'waited_correctly' @@ -69,19 +69,19 @@ module JamRuby # spec: https://jamkazam.atlassian.net/wiki/display/PS/Product+Specification+-+JamClass#ProductSpecification-JamClass-TeacherReceives&RespondstoLessonBookingRequest - if music_session.session_removed_at.nil? + if music_session.session_removed_at.nil? || (music_session.scheduled_start + (lesson_session.duration * 60)) < Time.now reason = SESSION_ONGOING bill = false else - if lesson_session.is_canceled? && lesson_session.canceled_by_teacher? && lesson_session.canceled_late? - # If the lesson was cancelled less than 24 hours before the start time by the teacher, then we do not bill the student. - teacher = LATE_CANCELLATION - bill = false - elsif lesson_session.is_canceled? && lesson_session.canceled_by_student? && lesson_session.canceled_late? - # If the lesson was cancelled less than 24 hours before the start time by the student (if that is even possible, I can’t remember now), then we do bill the student. - student = LATE_CANCELLATION - bill = true - elsif together_analysis[:session_time] / 60 > APP_CONFIG.lesson_together_threshold_minutes + #if lesson_session.is_canceled? && lesson_session.canceled_by_teacher? && lesson_session.canceled_late? + # # If the lesson was cancelled less than 24 hours before the start time by the teacher, then we do not bill the student. + # teacher = LATE_CANCELLATION + # bill = false + #elsif lesson_session.is_canceled? && lesson_session.canceled_by_student? && lesson_session.canceled_late? + # # If the lesson was cancelled less than 24 hours before the start time by the student (if that is even possible, I can’t remember now), then we do bill the student. + # student = LATE_CANCELLATION + # bill = true + if together_analysis[:session_time] / 60 > APP_CONFIG.lesson_together_threshold_minutes bill = true reason = SUCCESS elsif teacher_analysis[:joined_on_time] && teacher_analysis[:waited_correctly] diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index d63c798db..14947dd74 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -875,7 +875,7 @@ SQL result end - def scheduled_start_date + def scheduled_start_date if self.scheduled_start_time.blank? "" else @@ -936,20 +936,16 @@ SQL end duration end - # should create a timestamp like: - # - # with_timezone = TRUE - # Tuesday, April 29, 8:00-9:00 PM TIMEZONE (where TIMEZONE is the TIMEZONE defined in the MusicSession when it was created) - # - # with_timezone = FALSE - # Thursday, July 10 - 10:00pm - # this should be in a helper - def pretty_scheduled_start(with_timezone = true) + + def pretty_scheduled_start(with_timezone = true, shorter = false) if scheduled_start && scheduled_duration start_time = scheduled_start timezone_display = 'UTC' + utc_offset_display = '00:00' tz_identifier, tz_display = MusicSession.split_timezone(timezone) + short_tz = 'GMT' + begin tz = TZInfo::Timezone.get(tz_identifier) rescue Exception => e @@ -960,6 +956,15 @@ SQL begin start_time = tz.utc_to_local(scheduled_start.utc) timezone_display = tz_display + utc_offset_hours = tz.current_period.utc_total_offset / (60*60) + hour = sprintf '%02d', utc_offset_hours.abs + minutes = sprintf '%02d', ((tz.current_period.utc_total_offset.abs % 3600) / 3600) * 60 + utc_offset_display = "#{utc_offset_hours < 0 ? '-' : ' '}#{hour}:#{minutes}" + short_tz = start_time.strftime("%Z") + if short_tz == 'UTC' + short_tz = 'GMT' + end + rescue Exception => e @@log.error("unable to convert #{scheduled_start} to #{tz}, e=#{e}") puts "unable to convert #{e}" @@ -969,7 +974,11 @@ SQL duration = safe_scheduled_duration end_time = start_time + duration if with_timezone - "#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} #{timezone_display}" + if shorter + "#{start_time.strftime("%a, %b %e %Y")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} (#{short_tz}#{utc_offset_display})" + else + "#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} #{timezone_display}" + end else "#{start_time.strftime("%A, %B %e")} - #{start_time.strftime("%l:%M%P").strip}" end diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index d47ecff00..05298a1f5 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -63,7 +63,7 @@ module JamRuby end end - self.class.format_msg(self.description, {:user => source_user, target: target_user, :band => band, :session => session, purpose: purpose, student_directed: student_directed}) + self.class.format_msg(self.description, {:user => source_user, target: target_user, :band => band, :session => session, purpose: purpose, student_directed: student_directed, msg: message}) end # TODO: MAKE ALL METHODS BELOW ASYNC SO THE CLIENT DOESN'T BLOCK ON NOTIFICATION LOGIC @@ -135,6 +135,7 @@ module JamRuby session = options[:session] purpose = options[:purpose] student_directed = options[:student_directed] + msg = options[:msg] name, band_name = "" unless user.nil? @@ -271,6 +272,8 @@ module JamRuby end elsif purpose == 'reschedule' 'A lesson reschedule has been requested' + elsif purpose == 'chat' + notification_msg = "Lesson Message: #{msg}" end return notification_msg @@ -404,7 +407,7 @@ module JamRuby end end - def send_lesson_message(purpose, lesson_session, student_directed) + def send_lesson_message(purpose, lesson_session, student_directed, msg = nil) notification = Notification.new notification.description = NotificationTypes::LESSON_MESSAGE @@ -422,9 +425,11 @@ module JamRuby notification.session_id = lesson_session.music_session.id notification.lesson_session_id = lesson_session.id - notification_msg = format_msg(NotificationTypes::LESSON_MESSAGE, {purpose: purpose}) + notification_msg = format_msg(NotificationTypes::LESSON_MESSAGE, {purpose: purpose, msg: msg}) - #notification.message = notification_msg + if purpose == 'chat' + notification.message = msg + end notification.save @@ -756,7 +761,7 @@ module JamRuby end begin - UserMailer.teacher_scheduled_jamclass_invitation(music_session.lesson_session.teacher, notification_msg, music_session).deliver + #UserMailer.teacher_scheduled_jamclass_invitation(music_session.lesson_session.teacher, notification_msg, music_session).deliver rescue => e @@log.error("Unable to send SCHEDULED_JAMCLASS_INVITATION email to user #{music_session.lesson_session.teacher.email} #{e}") end @@ -796,7 +801,7 @@ module JamRuby end begin - UserMailer.student_scheduled_jamclass_invitation(student, notification_msg, music_session).deliver + #UserMailer.student_scheduled_jamclass_invitation(student, notification_msg, music_session).deliver rescue => e @@log.error("Unable to send SCHEDULED_JAMCLASS_INVITATION email to user #{student.email} #{e}") end @@ -1094,7 +1099,7 @@ module JamRuby # start in less than 24 hours, and haven't been # notified for a particular interval yet: def send_session_reminders - MusicSession.where("scheduled_start > NOW() AND scheduled_start <= (NOW()+INTERVAL '1 DAYS')").each do |candidate_session| + MusicSession.where("scheduled_start > NOW() AND scheduled_start <= (NOW()+INTERVAL '1 DAYS') AND lesson_session_id IS NULL").each do |candidate_session| tm = candidate_session.scheduled_start if (tm>(12.hours.from_now) && !notified?(candidate_session, NotificationTypes::SCHEDULED_SESSION_REMINDER_DAY)) # Send 24 hour reminders: diff --git a/ruby/lib/jam_ruby/models/sale_line_item.rb b/ruby/lib/jam_ruby/models/sale_line_item.rb index 61e753bbe..f5ca99b80 100644 --- a/ruby/lib/jam_ruby/models/sale_line_item.rb +++ b/ruby/lib/jam_ruby/models/sale_line_item.rb @@ -43,14 +43,17 @@ module JamRuby JamTrack.find_by_id(product_id) elsif product_type == GIFTCARD GiftCardType.find_by_id(product_id) + elsif product_type == LESSON + lesson_package_purchase else + raise 'unsupported product type' end end def product_info item = product - { name: product.name } if item + { name: product.name, product_type: product_type } if item end def state diff --git a/ruby/lib/jam_ruby/models/signup_hint.rb b/ruby/lib/jam_ruby/models/signup_hint.rb index 69640f35a..05ef88c1f 100644 --- a/ruby/lib/jam_ruby/models/signup_hint.rb +++ b/ruby/lib/jam_ruby/models/signup_hint.rb @@ -44,10 +44,25 @@ module JamRuby SignupHint.where("created_at < :week", {:week => 1.week.ago}).delete_all end - def self.most_recent_redirect(user, default) + def self.most_recent_redirect(user, default, queryParams=nil) + puts "jquery params" hint = SignupHint.where(user_id: user.id).order('created_at desc').first + if hint - hint.redirect_location + redirect = hint.redirect_location + puts "redirect #{redirect}" + uri = URI.parse(redirect) + bits = uri.query ? URI.decode_www_form(uri.query) : [] + if queryParams + queryParams.each do |k, v| + bits << [k, v] + end + end + + puts "bits #{bits}" + uri.query = URI.encode_www_form(bits) + puts "oh yeah #{uri.to_s}" + return uri.to_s else default end diff --git a/ruby/lib/jam_ruby/models/teacher.rb b/ruby/lib/jam_ruby/models/teacher.rb index a6982e944..5d0f583f9 100644 --- a/ruby/lib/jam_ruby/models/teacher.rb +++ b/ruby/lib/jam_ruby/models/teacher.rb @@ -145,7 +145,11 @@ module JamRuby def self.save_teacher(user, params) teacher = build_teacher(user, params) - teacher.save + if teacher.save + # flag the user as a teacher + teacher.user.is_a_teacher = true + teacher.user.save(validate: false) + end teacher end @@ -192,6 +196,7 @@ module JamRuby teacher.test_drives_per_week = params[:test_drives_per_week] if params.key?(:test_drives_per_week) teacher.school_id = params[:school_id] if params.key?(:school_id) + # Many-to-many relations: if params.key?(:genres) genres = params[:genres] @@ -317,7 +322,7 @@ module JamRuby end def has_stripe_billing? - false + user.has_stripe_connect? end def has_instruments_or_subject? diff --git a/ruby/lib/jam_ruby/models/teacher_distribution.rb b/ruby/lib/jam_ruby/models/teacher_distribution.rb index 585d8e61b..b124ed586 100644 --- a/ruby/lib/jam_ruby/models/teacher_distribution.rb +++ b/ruby/lib/jam_ruby/models/teacher_distribution.rb @@ -9,6 +9,28 @@ module JamRuby validates :teacher, presence: true validates :amount_in_cents, presence: true + def self.index(current_user, params) + limit = params[:per_page] + limit ||= 100 + limit = limit.to_i + + query = TeacherDistribution.where(teacher_id: current_user.id) + + current_page = params[:page].nil? ? 1 : params[:page].to_i + next_page = current_page + 1 + + # will_paginate gem + query = query.paginate(:page => current_page, :per_page => limit) + + if query.length == 0 # no more results + {query: query, next_page: nil} + elsif query.length < limit # no more results + {query: query, next_page: nil} + else + {query: query, next_page: next_page} + end + end + def self.create_for_lesson(lesson_session) distribution = create(lesson_session) distribution.lesson_session = lesson_session diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 3e187b491..de83de27b 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -1906,6 +1906,15 @@ module JamRuby !requested_test_drive(teacher).nil? end + def stripe_auth + user_authorizations.where(provider: "stripe_connect").first + end + + def has_stripe_connect? + auth = stripe_auth + auth && (!auth.token_expiration || auth.token_expiration > Time.now) + end + def fetch_stripe_customer Stripe::Customer.retrieve(stripe_customer_id) end @@ -1931,7 +1940,7 @@ module JamRuby customer end - def card_approved(token, zip) + def card_approved(token, zip, booking_id) approved_booking = nil User.transaction do @@ -1941,10 +1950,11 @@ module JamRuby self.stripe_customer_id = customer.id self.stored_credit_card = true if self.save - # we can also 'unlock' any booked sessions that still need to be done so - LessonBooking.unprocessed(self).each do |booking| - booking.card_approved - approved_booking = booking + if booking_id + approved_booking = LessonBooking.find_by_id(booking_id) + if approved_booking + approved_booking.card_approved + end end end end @@ -1985,7 +1995,7 @@ module JamRuby end end - booking = card_approved(params[:token], params[:zip]) + booking = card_approved(params[:token], params[:zip], params[:booking_id]) if params[:test_drive] self.reload result = Sale.purchase_test_drive(self, booking) @@ -1995,10 +2005,9 @@ module JamRuby self.reload end - intent = TeacherIntent.recent_test_drive(self) end - {lesson: booking, test_drive: test_drive, intent:intent, purchase: purchase} + {lesson: booking, test_drive: test_drive, purchase: purchase} end def requested_test_drive(teacher = nil) @@ -2031,6 +2040,12 @@ module JamRuby end end + def test_drive_declined(lesson_session) + # because we decrement test_drive credits as soon as you book, we need to bring it back now + self.remaining_test_drives = self.remaining_test_drives + 1 + self.save(validate: false) + end + def test_drive_failed(lesson_session) # because we decrement test_drive credits as soon as you book, we need to bring it back now self.remaining_test_drives = self.remaining_test_drives + 1 diff --git a/ruby/lib/jam_ruby/resque/scheduled/minutely_job.rb b/ruby/lib/jam_ruby/resque/scheduled/minutely_job.rb new file mode 100644 index 000000000..ad9df11a3 --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/minutely_job.rb @@ -0,0 +1,16 @@ +module JamRuby + class MinutelyJob + extend Resque::Plugins::JamLonelyJob + + @queue = :scheduled_minutely_job + @@log = Logging.logger[MinutelyJob] + + def self.perform + @@log.debug("waking up") + + LessonSession.minutely_check + + @@log.debug("done") + end + end +end diff --git a/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb b/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb index 8dc2eb3e6..2f3935b42 100644 --- a/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb +++ b/ruby/spec/jam_ruby/flows/monthly_recurring_lesson_spec.rb @@ -116,7 +116,7 @@ describe "Monthly Recurring Lesson Flow" do # puts del.inspect end # get acceptance emails, as well as 'your stuff is accepted' - UserMailer.deliveries.length.should eql 6 + UserMailer.deliveries.length.should eql 2 lesson_session.errors.any?.should be_false lesson_session.reload lesson_session.slot.should eql student_counter @@ -126,9 +126,10 @@ describe "Monthly Recurring Lesson Flow" do lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) booking.status.should eql LessonBooking::STATUS_APPROVED - UserMailer.deliveries.length.should eql 6 + UserMailer.deliveries.length.should eql 2 chat = ChatMessage.unscoped.order(:created_at).last - chat.message.should eql "Lesson Approved: 'Yeah I got this'" + chat.message.should eql 'Yeah I got this' + chat.purpose.should eql 'Lesson Approved' chat.channel.should eql ChatMessage::CHANNEL_LESSON chat.user.should eql teacher_user chat.target_user.should eql user @@ -185,7 +186,7 @@ describe "Monthly Recurring Lesson Flow" do lesson_purchase.sale_line_item.should eql line_item TeacherPayment.count.should eql 0 - TeacherPayment.daily_check + TeacherPayment.hourly_check teacher_distribution.reload teacher_distribution.distributed.should be_true TeacherPayment.count.should eql 1 diff --git a/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb b/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb index fa408e64d..9bb60d232 100644 --- a/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb +++ b/ruby/spec/jam_ruby/flows/normal_lesson_spec.rb @@ -71,9 +71,10 @@ describe "Normal Lesson Flow" do lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) booking.status.should eql LessonBooking::STATUS_APPROVED - UserMailer.deliveries.length.should eql 4 + UserMailer.deliveries.length.should eql 2 chat = ChatMessage.unscoped.order(:created_at).last - chat.message.should eql "Lesson Approved: 'Yeah I got this'" + chat.message.should eql 'Yeah I got this' + chat.purpose.should eql 'Lesson Approved' chat.channel.should eql ChatMessage::CHANNEL_LESSON chat.user.should eql teacher_user chat.target_user.should eql user @@ -351,9 +352,10 @@ describe "Normal Lesson Flow" do lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) booking.status.should eql LessonBooking::STATUS_APPROVED - UserMailer.deliveries.length.should eql 4 + UserMailer.deliveries.length.should eql 2 chat = ChatMessage.unscoped.order(:created_at).last - chat.message.should eql "Lesson Approved: 'Yeah I got this'" + chat.message.should eql 'Yeah I got this' + chat.purpose.should eql 'Lesson Approved' chat.channel.should eql ChatMessage::CHANNEL_LESSON chat.user.should eql teacher_user chat.target_user.should eql user diff --git a/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb b/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb index 3946eecdc..f91871129 100644 --- a/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb +++ b/ruby/spec/jam_ruby/flows/recurring_lesson_spec.rb @@ -107,7 +107,7 @@ describe "Recurring Lesson Flow" do # puts del.inspect end # get acceptance emails, as well as 'your stuff is accepted' - UserMailer.deliveries.length.should eql 6 + UserMailer.deliveries.length.should eql 2 lesson_session.errors.any?.should be_false lesson_session.reload lesson_session.slot.should eql student_counter @@ -117,9 +117,10 @@ describe "Recurring Lesson Flow" do lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) booking.status.should eql LessonBooking::STATUS_APPROVED - UserMailer.deliveries.length.should eql 6 + UserMailer.deliveries.length.should eql 2 chat = ChatMessage.unscoped.order(:created_at).last - chat.message.should eql "Lesson Approved: 'Yeah I got this'" + chat.message.should eql "Yeah I got this" + chat.purpose.should eql 'Lesson Approved' chat.channel.should eql ChatMessage::CHANNEL_LESSON chat.user.should eql teacher_user chat.target_user.should eql user diff --git a/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb b/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb index e9545cfbe..2fca45714 100644 --- a/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb +++ b/ruby/spec/jam_ruby/flows/testdrive_lesson_spec.rb @@ -134,7 +134,7 @@ describe "TestDrive Lesson Flow" do lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0) booking.status.should eql LessonBooking::STATUS_APPROVED - UserMailer.deliveries.length.should eql 4 + UserMailer.deliveries.length.should eql 2 chat = ChatMessage.unscoped.order(:created_at).last chat.message.should eql 'Yeah I got this' chat.channel.should eql ChatMessage::CHANNEL_LESSON @@ -145,7 +145,7 @@ describe "TestDrive Lesson Flow" do notification.student_directed.should eql true notification.purpose.should eql 'accept' notification.description.should eql NotificationTypes::LESSON_MESSAGE - notification.message.should eql "Your lesson request is confirmed!" + notification.message.should be_nil # teacher & student get into session @@ -194,7 +194,7 @@ describe "TestDrive Lesson Flow" do teacher_distribution.distributed.should be_false TeacherPayment.count.should eql 0 - TeacherPayment.daily_check + TeacherPayment.hourly_check TeacherPayment.count.should eql 1 lesson_session.reload diff --git a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb index 99fd536a5..d5f080cfc 100644 --- a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb +++ b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb @@ -783,6 +783,22 @@ describe AffiliatePartner do end end + describe "edge case" do + it "year change" do + partner.touch + last_day_of_year = Date.new(2015, 12, 31) + first_day_of_next_year = Date.new(2016, 01, 01) + AffiliatePartner.tally_up(last_day_of_year) + AffiliatePartner.tally_up(first_day_of_next_year) + quarterly_payment = AffiliateQuarterlyPayment.where(year: 2016, quarter: 0, affiliate_partner_id: partner.id).first + quarterly_payment.closed.should be_false + AffiliatePartner.tally_up(Date.new(2016, 01, 02)) + quarterly_payment.reload + quarterly_payment.closed.should be_false + end + end + + describe "boundary_dates_for_month" do it "invalid month" do expect{AffiliatePartner.boundary_dates_for_month(2015, 0)}.to raise_error diff --git a/ruby/spec/jam_ruby/models/lesson_booking_spec.rb b/ruby/spec/jam_ruby/models/lesson_booking_spec.rb index ac64a0f3a..4a17aea5b 100644 --- a/ruby/spec/jam_ruby/models/lesson_booking_spec.rb +++ b/ruby/spec/jam_ruby/models/lesson_booking_spec.rb @@ -426,7 +426,7 @@ describe LessonBooking do booking.errors.any?.should be false - chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message = ChatMessage.where(lesson_session_id: booking.next_lesson.id).first chat_message.should_not be_nil chat_message.message.should eq booking.description end @@ -488,7 +488,7 @@ describe LessonBooking do booking.lesson_type.should eq LessonBooking::LESSON_TYPE_TEST_DRIVE booking.lesson_booking_slots.length.should eq 2 - chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message = ChatMessage.where(lesson_session_id: booking.next_lesson.id).first chat_message.should_not be_nil chat_message.message.should eq booking.description @@ -502,7 +502,7 @@ describe LessonBooking do booking.errors.any?.should be false - chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message = ChatMessage.where(lesson_session_id: booking.next_lesson.id).first chat_message.should_not be_nil chat_message.message.should eq booking.description end @@ -549,7 +549,7 @@ describe LessonBooking do booking.lesson_type.should eq LessonBooking::LESSON_TYPE_PAID booking.lesson_booking_slots.length.should eq 2 - chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message = ChatMessage.where(lesson_session_id: booking.next_lesson.id).first chat_message.should_not be_nil chat_message.message.should eq booking.description @@ -570,7 +570,7 @@ describe LessonBooking do booking.lesson_type.should eq LessonBooking::LESSON_TYPE_PAID booking.lesson_booking_slots.length.should eq 2 - chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message = ChatMessage.where(lesson_session_id: booking.next_lesson.id).first chat_message.should_not be_nil chat_message.message.should eq booking.description @@ -579,12 +579,12 @@ describe LessonBooking do user.remaining_test_drives.should eq 1 end - it "allows long message to flow through chat" do + it "allows long message to flow through chat" do booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, Faker::Lorem.characters(10000), true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) booking.errors.any?.should be false - chat_message = ChatMessage.where(lesson_booking_id: booking.id).first + chat_message = ChatMessage.where(lesson_session_id: booking.next_lesson.id).first chat_message.should_not be_nil chat_message.message.should eq booking.description end @@ -709,6 +709,7 @@ describe LessonBooking do Timecop.freeze(7.days.ago) lesson_session.cancel({canceler: teacher_user, message: 'meh', slot: booking.default_slot.id, update_all: false}) lesson_session.errors.any?.should be_false + lesson_session.reload lesson_session.status.should eql LessonSession::STATUS_CANCELED lesson_session.reload booking.reload @@ -732,8 +733,8 @@ describe LessonBooking do Timecop.freeze(7.days.ago) lesson_session.cancel({canceler: user, message: 'meh', slot: booking.default_slot.id, update_all: false}) lesson_session.errors.any?.should be_false - lesson_session.status.should eql LessonSession::STATUS_CANCELED lesson_session.reload + lesson_session.status.should eql LessonSession::STATUS_CANCELED booking.reload booking.status.should eql LessonSession::STATUS_CANCELED UserMailer.deliveries.length.should eql 2 @@ -784,8 +785,8 @@ describe LessonBooking do Timecop.freeze(7.days.ago) lesson_session.cancel({canceler: user, message: 'meh', slot: booking.default_slot.id, update_all: true}) lesson_session.errors.any?.should be_false - lesson_session.status.should eql LessonSession::STATUS_CANCELED lesson_session.reload + lesson_session.status.should eql LessonSession::STATUS_CANCELED booking.reload booking.status.should eql LessonSession::STATUS_CANCELED booking.canceler.should eql user diff --git a/ruby/spec/jam_ruby/models/lesson_session_spec.rb b/ruby/spec/jam_ruby/models/lesson_session_spec.rb index e1470bdff..bbf60d93a 100644 --- a/ruby/spec/jam_ruby/models/lesson_session_spec.rb +++ b/ruby/spec/jam_ruby/models/lesson_session_spec.rb @@ -7,7 +7,7 @@ describe LessonSession do let(:slot1) { FactoryGirl.build(:lesson_booking_slot_single) } let(:slot2) { FactoryGirl.build(:lesson_booking_slot_single) } - let(:lesson_booking) {LessonBooking.book_normal(user, teacher, [slot1, slot2], "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60)} + let(:lesson_booking) {b = LessonBooking.book_normal(user, teacher, [slot1, slot2], "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60); b.card_presumed_ok = true; b.save!; b} let(:lesson_session) {lesson_booking.lesson_sessions[0]} describe "accept" do @@ -16,10 +16,28 @@ describe LessonSession do end end + describe "upcoming_sessions_reminder" do + it "succeeds" do + UserMailer.deliveries.clear + LessonSession.upcoming_sessions_reminder + lesson_session.touch + lesson_session.sent_starting_notice.should be_false + lesson_session.is_requested?.should be_true + lesson_session.music_session.scheduled_start = 15.minutes.from_now + lesson_session.music_session.save! + LessonSession.upcoming_sessions_reminder + UserMailer.deliveries.count.should eql 2 + UserMailer.deliveries.clear + lesson_session.reload + lesson_session.sent_starting_notice.should be_true + LessonSession.upcoming_sessions_reminder + UserMailer.deliveries.count.should eql 0 + end + end + describe "index" do it "finds single lesson as student" do - lesson_booking.touch lesson_session.touch lesson_session.music_session.creator.should eql lesson_session.lesson_booking.user lesson_session.lesson_booking.teacher.should eql teacher diff --git a/ruby/spec/jam_ruby/models/teacher_distribution_spec.rb b/ruby/spec/jam_ruby/models/teacher_distribution_spec.rb new file mode 100644 index 000000000..dbf54d562 --- /dev/null +++ b/ruby/spec/jam_ruby/models/teacher_distribution_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe TeacherDistribution do + + let(:teacher) {FactoryGirl.create(:teacher_user)} + + + describe "index" do + it "empty" do + TeacherDistribution.index(teacher, {})[:query].count.should eql 0 + end + + it "returns single" do + distribution = FactoryGirl.create(:teacher_distribution, teacher: teacher) + + TeacherDistribution.index(teacher, {})[:query].count.should eql 1 + + distribution = FactoryGirl.create(:teacher_distribution) # some random teacher + + TeacherDistribution.index(teacher, {})[:query].count.should eql 1 + end + end +end diff --git a/ruby/spec/mailers/render_emails_spec.rb b/ruby/spec/mailers/render_emails_spec.rb index 9fe7a4e37..659e172ac 100644 --- a/ruby/spec/mailers/render_emails_spec.rb +++ b/ruby/spec/mailers/render_emails_spec.rb @@ -49,6 +49,7 @@ describe "RenderMailers", :slow => true do @filename = "teacher_welcome_message" UserMailer.teacher_welcome_message(teacher).deliver end + it "teacher_lesson_request" do @filename = "teacher_lesson_request" @@ -120,6 +121,22 @@ describe "RenderMailers", :slow => true do UserMailer.deliveries.clear UserMailer.teacher_lesson_completed(lesson).deliver end + + it "lesson_starting_soon_teacher" do + @filename = "lesson_starting_soon_teacher" + lesson = testdrive_lesson(user, teacher) + + UserMailer.deliveries.clear + UserMailer.lesson_starting_soon_teacher(lesson).deliver + end + + it "lesson_starting_soon_student" do + @filename = "lesson_starting_soon_student" + lesson = testdrive_lesson(user, teacher) + + UserMailer.deliveries.clear + UserMailer.lesson_starting_soon_student(lesson).deliver + end end end diff --git a/web/Gemfile b/web/Gemfile index bde372771..b78176c6f 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -159,6 +159,7 @@ group :test, :cucumber do # gem 'growl', '1.0.3' gem 'poltergeist' gem 'resque_spec' + gem 'timecop' #gem 'thin' end diff --git a/web/app/assets/images/content/icon_unread_mail.png b/web/app/assets/images/content/icon_unread_mail.png new file mode 100644 index 0000000000000000000000000000000000000000..32f098bf51fed66a04697dafa25dcdd953aa9205 GIT binary patch literal 1537 zcmV+c2LAbpP)Kj;3NeXkuob5<(Ag?ot%wRm9A;GncW}HC^u`RL z=*}$m)C&aFTD1zh-A;Xd zJxi7>p|7uxNF>7G;2^0~ihMqgEX$wuBYlp6rfFEMR<>^4N?ThSHk%Ez*<50wC<@tZ zmeJ8so<4obg9i^77#KiNltru-02C$IxN###jvT>mw|7IL!O^?-4>!Sy_q0 z;lO6IVK5jNe>+a+KRda9|312|F92C2QCnLZKA(?2ee(^WV32=xb@A-kGjh2cLWoih zWLc)6p@Dt-_Tlk(@cDdHR8(;F>Q(aje950;u~^O&W$}8woIZV;Wy_Xv^Y`EB=;*-j z_b*8B{G^YKjnUK7Lpq(Np`n4MrY7R?IDtT*loQ2bv78Y?P+eWk$&)9sTCLo=b&K=A z{l8gif%EyMC<^&}p1!_5QmGUkj|aEgjoTXHjBk#DMkE8kfP3tqVVa{C+^<8i_7I=`}XbZ`Tlz%kqEh5ZVtd~HnVNpHWWqS z`t|EfO-*4i7}&gdGrM-}Drx`6-Y^=C1OfprT)03y9_Pl58+`ZY@9;D=v2x`~qR}XF zp-`ZRWb zGBQ%?;6x(9`Sa(wckdoPpO2Q779>fcP$&=#1~D3q*zI-{MVSL|xm*Y#n4Fv}$)RZ) z%a!5}p?H47T6=)+(z%r%-MNrXZn+S}Wy ztgJ+qWeSA?s;V+NI?AvA_?1m9n>cXb0Apig#N%<+uV2s0moGVY?i{MBVlWs=m1eP6 zP*s(|!9jFgN0nunfq?-snGANj9fQF@E|((~i}B0fe_>P0CVu$q4`ebKs;jH9TCJo~ zDH->+8`pjYuSd5CU12Wrl}`85tSDX0x$w-8u?|0^{T3 zcsw53+uKPcQ_RlJl1wIWe4tX{pEhQ!2u?Ab$kS$U}#BuSbB z&@_#Q4j)n2|E82tK~1Pt@wWanXIQWLaitXb4Ty@OV6I`1V`s>gt%Dp61=Vccm&R9$nY5*=!s-bcnzG_#y+GDQE3+7#+wEre?%jC3URJDFfyrb-2r*X~s*0v*OifMk`t@t> z+_}T^=g&*Be03)S=(>){WTK{~2B*`B-EPO}bYe1@=A)v8C@Q>m400000NkvXXu0mjf^VsO8 literal 0 HcmV?d00001 diff --git a/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee b/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee index 82dbe73eb..a605afd01 100644 --- a/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee +++ b/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee @@ -40,8 +40,6 @@ context.JK.AccountPaymentHistoryScreen = class AccountPaymentHistoryScreen @noMoreSales.hide() @next = null - - refresh:() => @currentQuery = this.buildQuery() @rest.getSalesHistory(@currentQuery) diff --git a/web/app/assets/javascripts/dialog/banner.js b/web/app/assets/javascripts/dialog/banner.js index 927f8b9b0..24a73219a 100644 --- a/web/app/assets/javascripts/dialog/banner.js +++ b/web/app/assets/javascripts/dialog/banner.js @@ -175,7 +175,10 @@ } else { $btn.click(function() { - button.click(); + if (button.click) { + button.click(); + } + hide(); return false; }); diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index b300cd4f2..b67879cba 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -62,7 +62,8 @@ CHECKOUT_SKIP_SIGN_IN: 'checkout_skip_sign_in', PREVIEW_PLAYED: 'preview_played', VST_OPERATION_SELECTED: 'vst_operation_selected', - VST_EFFECT_SELECTED: 'vst_effect_selected' + VST_EFFECT_SELECTED: 'vst_effect_selected', + LESSON_SESSION_ACTION: 'lesson_session_action' }; context.JK.PLAYBACK_MONITOR_MODE = { diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 2df970203..7a4cbd546 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -478,14 +478,6 @@ function getTeacher(options) { - // var url = '/api/teacher/detail' - - // if(options && _.size(options) > 0) { - // console.log("WTF"); - // url += "?" + $.param(options) - // } - - // console.log("THE URL", url) return $.ajax({ type: "GET", dataType: "json", @@ -1701,12 +1693,13 @@ }); } - function createChatMessage(options) { + function createChatMessage(data) { return $.ajax({ type: "POST", - url: '/api/chat?' + $.param(options), + url: '/api/chat', dataType: "json", - contentType: 'application/json' + contentType: 'application/json', + data: JSON.stringify(data) }); } @@ -2227,6 +2220,58 @@ }); } + + function getLesson(options) { + options = options || {} + return $.ajax({ + type: "GET", + url: "/api/lesson_sessions/" + options.id, + dataType: "json", + contentType: 'application/json' + }); + } + + + function updateLessonSessionUnreadMessages(options) { + return $.ajax({ + type: "POST", + url: '/api/lesson_sessions/' + options.id + "/update_unread_messages", + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function checkLessonReschedule(options) { + return $.ajax({ + type: "POST", + url: '/api/lesson_sessions/' + options.id + "/reschedule_check", + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + + function checkLessonCancel(options) { + return $.ajax({ + type: "POST", + url: '/api/lesson_sessions/' + options.id + "/cancel_check", + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function lessonStartTime(options) { + return $.ajax({ + type: "POST", + url: '/api/lesson_sessions/' + options.id + "/start_time", + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } function getTestDriveStatus(options) { return $.ajax({ type: "GET", @@ -2389,6 +2434,20 @@ }); } + function listTeacherDistributions(options) { + + if(options) { + options = {} + } + + return $.ajax({ + type: "GET", + url: "/api/teacher_distributions?" + $.param(options) , + dataType: "json", + contentType: 'application/json' + }); + } + function deleteSchoolTeacher(options) { var id = getId(options); @@ -2603,6 +2662,10 @@ this.counterLessonBooking = counterLessonBooking; this.submitStripe = submitStripe; this.getLessonSessions = getLessonSessions; + this.getLesson = getLesson; + this.updateLessonSessionUnreadMessages = updateLessonSessionUnreadMessages; + this.checkLessonCancel = checkLessonCancel; + this.checkLessonReschedule = checkLessonReschedule; this.getTestDriveStatus = getTestDriveStatus; this.createTeacherIntent = createTeacherIntent; this.getSchool = getSchool; @@ -2616,6 +2679,8 @@ this.resendSchoolInvitation = resendSchoolInvitation; this.deleteSchoolTeacher = deleteSchoolTeacher; this.deleteSchoolStudent = deleteSchoolStudent; + this.listTeacherDistributions = listTeacherDistributions; + this.lessonStartTime = lessonStartTime; return this; }; diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js index b13b42287..161576457 100644 --- a/web/app/assets/javascripts/jamkazam.js +++ b/web/app/assets/javascripts/jamkazam.js @@ -328,6 +328,8 @@ var screen = 'home' try { var location = context.RouteMap.parse(hash); + + screen = location.page.substring(1); // remove leading slash } catch (e) { diff --git a/web/app/assets/javascripts/jquery.lessonSessionActions.js b/web/app/assets/javascripts/jquery.lessonSessionActions.js new file mode 100644 index 000000000..0a03d6579 --- /dev/null +++ b/web/app/assets/javascripts/jquery.lessonSessionActions.js @@ -0,0 +1,91 @@ +(function(context, $) { + + "use strict"; + + context.JK = context.JK || {}; + + + // creates an iconic/graphical instrument selector. useful when there is minimal real-estate + + $.fn.lessonSessionActions = function(options) { + + return this.each(function(index) { + + function close() { + $parent.btOff(); + $parent.focus(); + } + + var $parent = $(this); + if($parent.data('lessonSessionActions')) { + // already constructed + return; + } + $parent.data('lessonSessionActions', options) + function onLessonActionSelected() { + var $li = $(this); + var lessonAction = $li.attr('data-lesson-option'); + + close(); + $parent.triggerHandler(context.JK.EVENTS.LESSON_SESSION_ACTION, {lessonAction: lessonAction, options: options}); + return false; + }; + + // if the user goes into the bubble, remove + function waitForBubbleHover($bubble) { + $bubble.hoverIntent({ + over: function() { + if(timeout) { + clearTimeout(timeout); + timeout = null; + } + }, + out: function() { + $parent.btOff(); + }}); + } + + var timeout = null; + + var html = context._.template($('#template-lesson-session-actions').html(), options, { variable: 'data' }) + + var extraClasses = ' ' + if(options.cardNotOk) { + extraClasses += 'not-card-ok ' + } + if(options.isRequested) { + extraClasses += 'is-requested ' + } + else if(options.isScheduled) { + extraClasses += 'is-scheduled ' + } + if(options.isAdmin) { + extraClasses += 'is-admin ' + } + context.JK.hoverBubble($parent, html, { + trigger:'none', + cssClass: 'lesson-action-popup' + extraClasses, + spikeGirth:0, + spikeLength:0, + width:250, + closeWhenOthersOpen: true, + offsetParent: $parent.closest('.screen'), + positions:['bottom'], + preShow: function() { + + }, + postShow:function(container) { + $(container).find('li').click(onLessonActionSelected) + if(timeout) { + clearTimeout(timeout); + timeout = null; + } + waitForBubbleHover($(container)) + timeout = setTimeout(function() {$parent.btOff()}, 3000) + } + }); + }); + } + + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index 85a435521..931f1bbaf 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -634,12 +634,14 @@ if(arguments.length == 1) { // user passed in dialog id var dialogId = arguments[0]; + var result = false context._.each(openDialogs, function(dialog) { if($(dialog).attr('layout-id') == dialogId) { - return true; + result = true; + return false; } }) - return false; + return result; } else { // user passed in nothing @@ -1071,7 +1073,7 @@ } this.isDialogShowing = function() { - return isDialogShowing(); + return isDialogShowing(arguments[0]); } this.activeElementEvent = function(evtName, data) { diff --git a/web/app/assets/javascripts/notificationPanel.js b/web/app/assets/javascripts/notificationPanel.js index fcf3b4474..aae85ffef 100644 --- a/web/app/assets/javascripts/notificationPanel.js +++ b/web/app/assets/javascripts/notificationPanel.js @@ -198,6 +198,8 @@ // register text messages registerTextMessage(); + + registerLessonMessage(); } function buildParams() { @@ -230,6 +232,9 @@ if(val.description == context.JK.MessageType.TEXT_MESSAGE) { val.formatted_msg = textMessageDialog.formatTextMessage(val.message.substring(0, 200), val.source_user_id, val.source_user.name, val.message.length > 200).html(); } + else if(val.description == context.JK.MessageType.LESSON_MESSAGE && val.message) { + val.formatted_msg = textMessageDialog.formatTextMessage(val.message.substring(0, 200), val.source_user_id, val.source_user.name, val.message.length > 200).html(); + } // fill in template for Connect pre-click var template = $notificationTemplate.html(); @@ -441,7 +446,13 @@ linkSessionInfoNotification(payload, $notification, $btnNotificationAction); } else if (type === context.JK.MessageType.LESSON_MESSAGE) { - linkLessonInfoNotification(payload, $notification, $btnNotificationAction); + if (payload.purpose == 'chat') { + linkLessonChatNotification(payload, $notification, $btnNotificationAction); + } + else + { + linkLessonInfoNotification(payload, $notification, $btnNotificationAction); + } } else if (type === context.JK.MessageType.SCHEDULED_JAMCLASS_INVITATION) { linkLessonInfoNotification(payload, $notification, $btnNotificationAction); @@ -494,6 +505,15 @@ }); } + function linkLessonChatNotification(payload, $notification, $btnNotificationAction) { + var $action_btn = $notification.find($btnNotificationAction); + $action_btn.text('LESSON CONVERSATION'); + $action_btn.click(function() { + gotoLessonChatPage({"lesson_session_id": payload.lesson_session_id}); + }); + } + + function acceptBandInvitation(args) { rest.updateBandInvitation( args.band_id, @@ -1287,6 +1307,37 @@ }); } + function registerLessonMessage() { + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.LESSON_MESSAGE, function(header, payload) { + logger.debug("Handling LESSON_MESSAGE msg " + JSON.stringify(payload)); + + //context.ChatActions.msgReceived(payload); + + // only show if chat dialog is not showing or if the focused lesson session is not the one specified in the payload + console.log("context.ChatStore.lessonSessionId", app.layout.isDialogShowing('chat-dialog'), context.ChatStore.lessonSessionId, payload.lesson_session_id) + if (!app.layout.isDialogShowing('chat-dialog') || context.ChatStore.lessonSessionId != payload.lesson_session_id) { + app.notify({ + "title": "Lesson Message", + "text": payload.msg, + "icon_url": context.JK.resolveAvatarUrl(payload.photo_url) + }, [{ + id: "btn-lesson-chat", + text: "SEE LESSON CONVERSATION", + "layout-action": "close", + href: "#", + "class": "button-orange", + callback: gotoLessonChatPage, + callback_args: { + "lesson_session_id": payload.lesson_session_id + } + }] + ); + } + + handleNotification(payload, header.type); + }); + } + function registerBandInvitationAccepted() { context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_INVITATION_ACCEPTED, function(header, payload) { logger.debug("Handling BAND_INVITATION_ACCEPTED msg " + JSON.stringify(payload)); @@ -1380,6 +1431,11 @@ window.location.href = "/client#/jamclass/lesson-booking/" + args.lesson_session_id } + function gotoLessonChatPage(args) { + app.layout.showDialog('chat-dialog', {d1: "lesson_" + args.lesson_session_id}) + //window.location.href = "/client#/jamclass/chat-dialog/d1=lesson_" + args.lesson_session_id + } + function deleteNotificationHandler(evt) { evt.stopPropagation(); var notificationId = $(this).attr('notification-id'); diff --git a/web/app/assets/javascripts/react-components/AccountPaymentHistoryScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/AccountPaymentHistoryScreen.js.jsx.coffee new file mode 100644 index 000000000..786585d8f --- /dev/null +++ b/web/app/assets/javascripts/react-components/AccountPaymentHistoryScreen.js.jsx.coffee @@ -0,0 +1,382 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +AppStore = context.AppStore +UserStore = context.UserStore + +profileUtils = context.JK.ProfileUtils + +@AccountPaymentHistoryScreen = React.createClass({ + + mixins: [ + #ICheckMixin, + Reflux.listenTo(AppStore, "onAppInit"), + Reflux.listenTo(UserStore, "onUserChanged") + ] + + shownOnce: false + screenVisible: false + + LIMIT: 20 + TILE_PAYMENTS_TO_YOU: 'payments to you' + TILE_PAYMENTS_TO_JAMKAZAM: 'payments to jamkazam' + TILE_PAYMENT_METHOD: 'payment method' + + STUDENT_TILES: ['payments to jamkazam', 'payment method'] + TEACHER_TILES: ['payments to you', 'payments to jamkazam'] + + onAppInit: (@app) -> + @app.bindScreen('account/paymentHistory', {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide}) + + onUserChanged: (userState) -> + @setState({user: userState?.user}) + + componentDidMount: () -> + #@checkboxes = [{selector: 'input.slot-decision', stateKey: 'slot-decision'}] + @root = $(@getDOMNode()) + @endOfList = @root.find('.end-of-payments-list') + @contentBodyScroller = @root + #@iCheckify() + + registerInfiniteScroll:() -> + $scroller = @contentBodyScroller + logger.debug("registering infinite scroll") + $scroller.off('scroll') + $scroller.on('scroll', () => + # be sure to not fire off many refreshes when user hits the bottom + return if @refreshing + + if $scroller.scrollTop() + $scroller.innerHeight() + 100 >= $scroller[0].scrollHeight + #$scroller.append('
... Loading more Payments ...
') + @setState({searching: true}) + logger.debug("refreshing more payments for infinite scroll") + @nextPage() + ) + + nextPage: () -> + #nextPage = @state.salesCurrentPage + 1 + @incrementCurrentPage() + @refresh() + + activeListItems: () -> + tile = @activeTile() + if tile == @TILE_PAYMENTS_TO_YOU + this.state.teacher_distributions + else if tile == @TILE_PAYMENTS_TO_JAMKAZAM + this.state.sales + else + [] + + componentDidUpdate: () -> + #@iCheckify() + + if @state.next == null + @contentBodyScroller.off('scroll') + if @state.salesCurrentPage == 1 and @state.sales.length == 0 + @endOfList.show() + logger.debug("PaymentHistoryScreen: empty search") + else if @state.salesCurrentPage > 0 + logger.debug("end of search") + @endOfList.show() + else + @registerInfiniteScroll(@contentBodyScroller) + + checkboxChanged: (e) -> + checked = $(e.target).is(':checked') + + value = $(e.target).val() + + @setState({userSchedulingComm: value}) + + beforeHide: (e) -> + @screenVisible = false + + beforeShow: (e) -> + + afterShow: (e) -> + @clearResults() + @screenVisible = true + @refresh() + + + refresh: () -> + @buildQuery() + if @activeTile() == @TILE_PAYMENTS_TO_YOU + @refreshTeacherDistributions() + else if @activeTile() == @TILE_PAYMENTS_TO_JAMKAZAM + @refreshSales() + + refreshSales: () -> + @refreshing = true + rest.getSalesHistory(@currentQuery) + .done(@salesHistoryDone) + .fail(@salesHistoryFail) + + refreshTeacherDistributions: () -> + @refreshing = true + rest.listTeacherDistributions(@currentQuery) + .done(@teacherDistributionsDone) + .fail(@teacherDistributionsFail) + + salesHistoryDone:(response) -> + @refreshing = false + this.setState({next: response.next, sales: this.state.sales.concat(response.entries)}) + + salesHistoryFail:(jqXHR) -> + @refreshing = false + @app.notifyServerError jqXHR, 'Payments to JamKazam Unavailable' + + teacherDistributionsDone:(response) -> + @refreshing = false + this.setState({next: response.next, distributions: this.state.distributions.concat(response.entries)}) + + teacherDistributionsFail:(jqXHR) -> + @refreshing = false + @app.notifyServerError jqXHR, 'Payments to You Unavailable' + + + clearResults:() -> + this.setState({salesCurrentPage: 0, sales: [], distributionsCurrentPage: [], distributions: 0, next: null}) + + buildQuery:(page = @getCurrentPage()) -> + console.log("PAGE!", page) + @currentQuery = this.defaultQuery(page) + + defaultQuery:(page = @getCurrentPage()) -> + query = + per_page: @LIMIT + page: page + 1 + if this.state.next + query.since = this.state.next + query + + getCurrentPage: () -> + page = this.state[@getCurrentPageName()] + if !page? + page = 1 + page + + incrementCurrentPage: () -> + newState = {} + newState[@getCurrentPageName] = @state[@getCurrentPageName()] + 1 + this.setState(newState) + + getCurrentPageName: () -> + if @activeTile() == @TILE_PAYMENTS_TO_JAMKAZAM + 'salesCurrentPage' + else if @activeTile() == @TILE_PAYMENTS_TO_YOU + 'distributionsCurrentPage' + else + 1 + + getInitialState: () -> + { + user: null, + nextPager: null, + salesCurrentPage: 0, + distributionsCurrentPage: 0 + next: null, + sales: [], + distributions: [] + selected: null + } + + onCancel: (e) -> + e.preventDefault() + context.location.href = '/client#/account' + + + mainContent: () -> + if !@state.user? + `
Loading...
` + else if @state.selected == @TILE_PAYMENTS_TO_YOU + @paymentsToYou() + else if @state.selected == @TILE_PAYMENTS_TO_JAMKAZAM + @paymentsToJamKazam() + else if @state.selected == @TILE_PAYMENT_METHOD + @paymentMethod() + else + @paymentsToJamKazam() + + paymentsToYou: () -> + + paymentMethod: () -> + + + paymentsToJamKazam: () -> + rows = [] + + for paymentHistory in this.state.sales + paymentMethod = 'Credit Card' + if paymentHistory.sale? + sale = paymentHistory.sale + amt = sale.recurly_total_in_cents + status = 'paid' + displayAmount = ' $' + (amt/100).toFixed(2) + date = context.JK.formatDate(sale.created_at, true) + items = [] + for line_item in sale.line_items + items.push(line_item.product_info?.name) + description = items.join(', ') + else + # this is a recurly webhook + transaction = paymentHistory.transaction + amt = transaction.amount_in_cents + status = transaction.transaction_type + displayAmount = '($' + (amt/100).toFixed(2) + ')' + date = context.JK.formatDate(transaction.transaction_at, true) + description = transaction.admin_description + + amountClasses = {status: status} + + row = + ` + {date} + {paymentMethod} + {description} + {status} + {displayAmount} + ` + rows.push(row) + + `
+ + + + + + + + + + + + {rows} + + +
DATEMETHODDESCRIPTIONSTATUSAMOUNT
+ Next +
No more payment history
+
+ BACK +
+
+
` + + selectionMade: (selection, e) -> + e.preventDefault() + @setState({selected: selection}) + + activeTile: () -> + if this.state.selected? + this.state.selected + else + @tiles()[0] + + createTileLink: (i, tile) -> + if this.state.selected? + active = this.state.selected == tile + else + active = i == 0 + + + tileClasses = {activeTile: active, 'profile-tile': true} + tileClasses[@myRole()] = true + tileClasses = classNames(tileClasses) + + classes = classNames({last: i == @tiles().length - 1}) + + return `` + + tiles: () -> + if @viewerStudent() + tiles = @STUDENT_TILES + else + tiles = @TEACHER_TILES + tiles + + myRole: () -> + if @viewerStudent() + 'student' + else + 'teacher' + + viewerStudent: () -> + !@viewerTeacher() + + viewerTeacher: () -> + this.state.user?.is_a_teacher + + onCustomBack: (customBack, e) -> + e.preventDefault() + context.location = customBack + + render: () -> + mainContent = @mainContent() + + profileSelections = [] + for tile, i in @tiles() + profileSelections.push(@createTileLink(i, tile, profileSelections)) + + profileNavClasses = {"profile-nav": true} + profileNavClasses[@myRole()] = true + profileNavClasses = classNames(profileNavClasses) + profileNav = `
+ {profileSelections} +
` + + `
+
+
payment
history:
+ {profileNav} +
+
+ +
+
+
+ {mainContent} +
+
+
+
+
` + + onUpdate: (e) -> + e.preventDefault() + + if this.state.updating + return + name = @root.find('input[name="name"]').val() + if @isSchoolManaged() + scheduling_communication = 'school' + else + scheduling_communication = 'teacher' + correspondence_email = @root.find('input[name="correspondence_email"]').val() + + @setState(updating: true) + rest.updateSchool({ + id: this.state.school.id, + name: name, + scheduling_communication: scheduling_communication, + correspondence_email: correspondence_email + }).done((response) => @onUpdateDone(response)).fail((jqXHR) => @onUpdateFail(jqXHR)) + + onUpdateDone: (response) -> + @setState({school: response, userSchedulingComm: null, schoolName: null, updateErrors: null, updating: false}) + + @app.layout.notify({title: "update success", text: "Your school information has been successfully updated"}) + + onUpdateFail: (jqXHR) -> + handled = false + + @setState({updating: false}) + + if jqXHR.status == 422 + errors = JSON.parse(jqXHR.responseText) + handled = true + @setState({updateErrors: errors}) + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee b/web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee index 72edef0cd..97793c160 100644 --- a/web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee @@ -181,6 +181,8 @@ UserStore = context.UserStore onBookLesson: (e) -> e.preventDefault() + logger.debug("user requested to book lesson") + if $(e.target).is('.disabled') return @@ -217,6 +219,7 @@ UserStore = context.UserStore else throw "Unable to determine lesson type" + logger.debug("lesson booking data: " + JSON.stringify(options)) @resetErrors() @setState({updating: true}) rest.bookLesson(options).done((response) => @booked(response)).fail((jqXHR) => @failedBooking(jqXHR)) @@ -224,13 +227,14 @@ UserStore = context.UserStore booked: (response) -> @setState({updating: false}) if response.user['has_stored_credit_card?'] - context.location ="/client#/jamclass/lesson-session/" + response.id + context.location = "/client#/jamclass/lesson-booking/#{response.id}" else - context.location = '/client#/jamclass/payment' + context.location = "/client#/jamclass/lesson-payment/lesson-booking_#{response.id}" failedBooking: (jqXHR) -> @setState({updating: false}) if jqXHR.status == 422 + logger.debug("unable to book lesson: " + jqXHR.responseText) body = JSON.parse(jqXHR.responseText) generalErrors = {errors: {}} @@ -333,7 +337,7 @@ UserStore = context.UserStore am_pm = [``, ``] - bookLessonClasses = classNames({"button-orange": true, disabled: !this.state.teacher? && !@state.updating}) + bookLessonClasses = classNames({"button-orange": true, 'book-lesson-btn': true, disabled: !this.state.teacher? && !@state.updating}) cancelClasses = classNames({"button-grey": true, disabled: !this.state.teacher? && !@state.updating}) descriptionErrors = context.JK.reactSingleFieldErrors('description', @state.descriptionErrors) @@ -357,7 +361,7 @@ UserStore = context.UserStore
- +
@@ -371,7 +375,7 @@ UserStore = context.UserStore
- +
@@ -443,7 +447,7 @@ UserStore = context.UserStore {slots}
Tell {teacher_first_name} a little about yourself as a student.
- + SEND + {closeBtn} + {emailSentNotice}
` + activeChannelType: () -> + if this.props?.channelType? + this.props.channelType + else + @state.channel + sendMessage:()-> if !context.JK.JamServer.connected return false @@ -92,7 +150,8 @@ ChatActions = @ChatActions if !@sendingMessage @sendingMessage = true - ChatActions.sendMsg(msg, @sendMsgDone, @sendMsgFail) + target_user_id = this.props?.other?.id + ChatActions.sendMsg(msg, @sendMsgDone, @sendMsgFail, target_user_id, @activeChannelType()) sendMsgDone: () -> @textBox.val('') @@ -109,6 +168,10 @@ ChatActions = @ChatActions e.preventDefault() @sendMessage() + handleCloseMessage: (e) -> + e.preventDefault() + this.props.onCloseClicked() + componentDidMount: () -> @root = $(@getDOMNode()) @textBox = @root.find('textarea') diff --git a/web/app/assets/javascripts/react-components/JamClassScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamClassScreen.js.jsx.coffee new file mode 100644 index 000000000..07219f9d9 --- /dev/null +++ b/web/app/assets/javascripts/react-components/JamClassScreen.js.jsx.coffee @@ -0,0 +1,496 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger + +UserStore = context.UserStore + +@JamClassScreen = React.createClass({ + + mixins: [ + @ICheckMixin, + @PostProcessorMixin, + Reflux.listenTo(AppStore, "onAppInit"), + Reflux.listenTo(UserStore, "onUserChanged") + ] + + lookup : { + name_specified: {name: 'Profile', profile: ''}, + experiences_teaching: {name: 'Experience', teacher_profile: "experience"}, + experiences_education: {name: 'Experience', teacher_profile: "experience" }, + experiences_award: {name: 'Experience', teacher_profile: "experience" }, + has_stripe_account: {name: 'Stripe', text: "Press the 'Stripe Connect' button in the bottom-left of the page"}, + has_teacher_bio: {name: 'Introduction', teacher_profile: "introduction"}, + intro_video: {name: 'Introduction', teacher_profile: "introduction"}, + years_teaching: {name: 'Introduction', teacher_profile: "introduction"}, + years_playing: {name: 'Introduction', teacher_profile: "introduction"}, + instruments_or_subject: {name: 'Basics', teacher_profile: "basics"}, + genres: {name: 'Basics', teacher_profile: "basics"}, + languages: {name: 'Basics', teacher_profile: "basics"}, + teaches_ages_specified: {name: 'Basics', teacher_profile: "basics"}, + teaching_level_specified: {name: 'Basics', teacher_profile: "basics"}, + has_pricing_specified: {name: 'Pricing', teacher_profile: "pricing"} + } + + onAppInit: (@app) -> + @app.bindScreen('jamclass', + {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide}) + + onUserChanged: (userState) -> + @setState({user: userState?.user}) + + componentDidMount: () -> + @root = $(@getDOMNode()) + + componentDidUpdate: () -> + items = @root.find('.jamtable tbody td.actionsColumn .lesson-session-actions-btn') + + $.each(items, (i, node) => ( + $node = $(node) + + lesson = @findLesson($node.attr('data-lesson-id')) + + $node.lessonSessionActions(lesson).on(context.JK.EVENTS.LESSON_SESSION_ACTION, @lessonSessionActionSelected) + )) + + lessonSessionActionSelected: (e, data) -> + + lessonId = data.options.id + lesson = @findLesson(lessonId) + + if data.lessonAction == 'status' + # show the status of the lesson + window.location.href = '/client#/jamclass/lesson-booking/' + lessonId + else if data.lessonAction == 'messages' + @app.layout.showDialog('chat-dialog', {d1: 'lesson_' + lessonId}) + else if data.lessonAction == 'cancel' + @cancelLesson(lesson) + # @@app.layout.showDialog('cancel-lesson-dialog', {d1: lessonId}) + else if data.lessonAction == 'join' + window.location.href = '/client#/session/' + lesson.music_session.id + else if data.lessonAction == 'reschedule' + @rescheduleLesson(lesson) + else if data.lessonAction == 'start-5-min' + rest.lessonStartTime({id: lessonId, minutes: 5}).done((response) => (@app.layout.notify({title: 'Start Time Set', text: "Start time for session set to 5 mins from now"}))) + else if data.lessonAction == 'start-35-ago' + rest.lessonStartTime({id: lessonId, minutes: -35}).done((response) => (@app.layout.notify({title: 'Start Time Set', text: "Start time for session set to 35 mins ago"}))) + else if data.lessonAction == 'enter-payment' + window.location.href = "/client#/jamclass/lesson-payment/lesson-booking_#{lessonId}" + else + context.JK.showAlert('unknown lesson action', 'The option in the menu is unknown') + + findLesson: (lessonId) -> + for lesson in @lessons() + if lessonId == lesson.id + return lesson + + return null + + rescheduleSelected: (lesson, recurring) -> + rest.checkLessonReschedule({id: lesson.id, update_all: recurring}) + .done((response) => ( + if recurring + window.location.href = '/client#/jamclass/lesson-booking/' + lesson.lesson_booking_id + else + window.location.href = '/client#/jamclass/lesson-booking/' + lesson.id + )) + .fail((jqXHR) => ( + if jqXHR.status == 422 + + if recurring + if @viewerStudent() + context.JK.Banner.showAlert("Policy Issue", "

We’re sorry, but you cannot reschedule this recurring lesson right now because it is less than 24 hours before the lesson start time. This is not allowed in the terms of service because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost. You may reschedule this recurring lesson anytime starting from the end of this next scheduled lesson, so please plan to do it then.

") + else + context.JK.Banner.showAlert("Policy Issue", "

We’re sorry, but you cannot reschedule this recurring lesson right now because it is less than 24 hours before the lesson start time. This is not allowed in the terms of service. You may reschedule this recurring lesson anytime starting from the end of this next scheduled lesson, so please plan to do it then.

") + else + if @viewerStudent() + context.JK.Banner.showAlert("Policy Issue", "

We’re sorry, but you cannot reschedule a lesson less than 24 hours before the lesson start time. This is not allowed in the terms of service because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost.

") + else + context.JK.Banner.showAlert("Policy Issue", "

We’re sorry, but you cannot reschedule a lesson less than 24 hours before the lesson start time. This is not allowed in the terms of service.

") + else + @app.ajaxError(jqXHR) + )) + + refreshLesson: (lessonId) -> + rest.getLesson({id: lessonId}).done((response) => @refreshLessonDone(response)).fail((jqXHR) => @refreshLessonFail(jqXHR)) + + refreshLessonDone: (lesson_session) -> + @updateLessonState(lesson_session) + + refreshLessonFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + issueCancelLesson: (lesson, update_all) -> + request = {} + request.message = '' + request.id = lesson.lesson_booking_id + request.lesson_session_id = lesson.id + request.update_all = update_all + + rest.cancelLessonBooking(request).done((response) => @cancelLessonBookingDone(response, lesson)).fail((response) => @cancelLessonBookingFail(response)) + + cancelLessonBookingDone: (booking, lesson) -> + if booking.focused_lesson.teacher_short_canceled + if @teacherViewing() + context.JK.Banner.showAlert('late cancellation warning', 'Cancelling a lesson less than 24 hours before it’s scheduled to start should be avoided, as it’s an inconvenience to the student. Repeated violations of this policy will negatively affect your teacher score.') + + @refreshLesson(lesson.id) + + cancelLessonBookingFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + cancelSelected: (lesson, recurring) -> + rest.checkLessonCancel({id: lesson.id, update_all: recurring}).done((response) => (@issueCancelLesson(lesson, recurring))).fail((jqXHR) => (@cancelSelectedFail(jqXHR))) + + cancelSelectedFailed: (jqXHR) -> + if jqXHR.status == 422 + + if recurring + if @viewerStudent() + buttons = [] + buttons.push({name: 'CLOSE', buttonStyle: 'button-grey'}) + buttons.push({name: 'CANCEL RECURRING LESSONS', buttonStyle: 'button-orange', click:(() => (@issueCancelLesson(lesson, true)))}) + context.JK.Banner.show({title: "Policy Issue", html: "You may cancel this recurring series of lessons, but it is too late to cancel the next scheduled lesson because it is less than 24 hours before the lesson start time. This is not allowed in the terms of service because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost.", buttons: buttons}) + else + # path should not be taken + context.JK.Banner.showAlert("Policy Issue", "

We’re sorry, but you cannot cancel a lesson less than 24 hours before the lesson start time. This is not allowed in the terms of service because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost.

") + else + if @viewerStudent() + context.JK.Banner.showAlert("Policy Issue", "

We’re sorry, but you cannot cancel a lesson less than 24 hours before the lesson start time. This is not allowed in the terms of service because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost.

") + else + # path should not be taken + context.JK.Banner.showAlert("Policy Issue", "

We’re sorry, but you cannot cancel a lesson less than 24 hours before the lesson start time. This is not allowed in the terms of service.

") + else + @app.ajaxError(jqXHR) + + rescheduleLesson: (lesson) -> + if lesson.recurring + buttons = [] + buttons.push({name: 'THIS SESSION', buttonStyle: 'button-orange', click:(() => (@rescheduleSelected(lesson, false)))}) + buttons.push({name: 'ALL SESSIONS', buttonStyle: 'button-orange', click:(() => (@rescheduleSelected(lesson, true)))}) + context.JK.Banner.show({title: 'Rescheduling Selection', html:'Do you wish to all reschedule all lessons or just the one selected?', buttons: buttons}) + else + @rescheduleSelected(lesson, false) + + + cancelLesson: (lesson) -> + + if lesson.isRequested + confirmTitle = 'Confirm Decline' + verbLower = 'decline' + else + confirmTitle = 'Confirm Cancelation' + verbLower = 'cancel' + if !lesson.isRequested || lesson.recurring + buttons = [] + buttons.push({name: 'CANCEL', buttonStyle: 'button-grey'}) + buttons.push({name: 'THIS SESSION', buttonStyle: 'button-orange', click:(() => (@cancelSelected(lesson, false)))}) + buttons.push({name: 'ALL SESSIONS', buttonStyle: 'button-orange', click:(() => (@cancelSelected(lesson, true)))}) + context.JK.Banner.show({title: 'Select One', html:"Do you wish to all #{verbLower} all lessons or just the one selected?", buttons: buttons}) + else + context.JK.Banner.showYesNo({title: confirmTitle, html:"Are you sure you want to #{verbLower} this lesson?", yes: () =>(@cancelSelected(lesson, lesson.recurring))}) + + getInitialState: () -> + { + user: null, + } + + beforeHide: (e) -> + + + beforeShow: (e) -> + + afterShow: (e) -> + @checkStripeSuccessReturn() + @setState({updating: true}) + rest.getLessonSessions().done((response) => @jamClassLoaded(response)).fail((jqXHR) => @failedJamClassLoad(jqXHR)) + + checkStripeSuccessReturn: () -> + if $.QueryString['stripe-success']? + if $.QueryString['stripe-success'] == 'true' + context.JK.Banner.showNotice('stripe connected', 'Congratulations, you have successfully connected your Stripe account, so payments for student lessons can now be processed.') + + if window.history.replaceState #ie9 proofing + window.history.replaceState({}, "", "/client#/jamclass") + + resetState: () -> + @setState({updating: false, lesson: null}) + + jamClassLoaded: (response) -> + @setState({updating: false}) + + @postProcess(response) + + @setState({lesson_sessions: response}) + + failedJamClassLoad: (jqXHR) -> + @setState({updating: false}) + if jqXHR.status == 404 + @app.layout.notify({title: "Unable to load JamClass info", text: "Try refreshing the web page"}) + + lessons: () -> + if @state.lesson_sessions? + @state.lesson_sessions.entries + else + [] + + postProcess: (data) -> + + for lesson in data.entries + + # calculate: + # .other (user object), + # .me (user object), + # other/me.musician_profile (link to musician profile) + # other/me.resolved_photo_url + # .hasUnreadMessages + # .isRequested (doesn't matter if countered or not). but can't be done + # .isScheduled (doesn't matter if countered or not). + @postProcessLesson(lesson) + + onReadMessage: (lesson_session, e) -> + e.preventDefault() + + data = {id: lesson_session.id} + data["#{this.myRole()}_unread_messages"] = false + + rest.updateLessonSessionUnreadMessages(data).done((response) => @updatedLessonSessionReadDone(response)).fail((jqXHR) => @updatedLessonSessionReadFail(jqXHR)) + + updateLessonState: (lesson_session) -> + @postProcessLesson(lesson_session) + + for lesson in @lessons() + if lesson.id == lesson_session.id + $.extend(lesson, lesson_session) + @setState({lesson_sessions: this.state.lesson_sessions}) + + updatedLessonSessionReadDone: (lesson_session) -> + # update lesson session data in local state + + @updateLessonState(lesson_session) + + @app.layout.showDialog('chat-dialog', {d1: 'lesson_' + lesson_session.id}) + + + updatedLessonSessionReadFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + + openMenu: (lesson, e) -> + $this = $(e.target) + if !$this.is('.lesson-session-actions-btn') + $this = $this.closest('.lesson-session-actions-btn') + $this.btOn() + + constructMissingLinks: (user) -> + + links = [] + + matches = {} + for problem, isPresent of user.teacher.profile_pct_summary + data = @lookup[problem] + if data? && !isPresent + matches[data.name] = data + + for name, data of matches + if !data.text? + links.push(`{data.name}`) + + for name, data of matches + if data.text? + links.push(`{data.name}`) + `
    {links}
` + + onProfileHelp: (data, e) -> + e.preventDefault() + @app.layout.notify({title: data.name, text: data.text}) + + missingProfileClick: (data, e) -> + if data.profile? + window.ProfileActions.startProfileEdit(data.profile, true) + else if data.teacher_profile? + window.ProfileActions.startTeacherEdit(data.teacher_profile, true) + else + throw "unknown type in data " + JSON.stringify(data) + + viewTeacherProfile: (e) -> + e.preventDefault() + window.ProfileActions.viewTeacherProfile(this.state.user, '/client#/jamclass', 'BACK TO JAMCLASS HOME') + + render: () -> + + disabled = @state.updating + + if !@state.user?.id + return `
Loading
` + + if @viewerStudent() + searchTeachers = `
+

search teachers

+ +

JamClass instructors are each individually screened to ensure that they are highly qualified music + teachers, + equipped to teach effectively online, and background checked. +

+ + +
` + learnMoreLink = `LEARN MORE` + learnMoreAboutJamClass = `

+ JamClass is the best way to take music lessons, offering significant advantages over both traditional + face-to-face lessons + and online skype lessons. +

` + signupTestDrive = `
+

sign up for testdrive

+ +

+ There are two awesome, painless ways to get started with JamClass. +

+ +

+ Sign up for TestDrive and take 4 full 30-minute lessons - one each from 4 different instructors - for just + $49.99. + You wouldn't marry the first person you date, right? Find the best teacher for you. It's the most important + factor in the success for your lessons! +

+ +

+ Or take one JamClass lesson free. It's on us! We're confident you'll take more. +

+ +

+ Sign up for TestDrive using the button below, or to take one free lesson, search our teachers, and click the + Book Free Lesson on your favorite. +

+ + +
` + else + searchTeachers = `
+

stripe status

+
+ +
+
` + learnMoreLink = `LEARN MORE` + learnMoreAboutJamClass = `

+ JamClass is the best way to teach music lessons, offering significant advantages over both traditional + face-to-face lessons + and online skype lessons. +

` + if this.state.user? + teacherProfileUri = "/client#/profile/teacher/#{this.state.user.id}" + pct = Math.round(this.state.user.teacher?.profile_pct) + + if pct == 100 + pctCompleteMsg = `

Your teacher profile is {pct}% complete.

` + else + pctCompleteMsg = `

Your teacher profile is {pct}% complete. The following sections of your profile are missing information. Click any of these links to view and add missing information:

` + missingLinks = @constructMissingLinks(this.state.user) + + signupTestDrive = `
+ {pctCompleteMsg} + {missingLinks} + VIEW TEACHER PROFILE +
` + + classes = [] + if @state.updating + classes = [` + Loading... + `] + else + for lessonData in @lessons() + + if lessonData.hasUnreadMessages + unreadMessages = `` + else + unreadMessages = null + lesson = ` +
{lessonData.other.first_name}
{lessonData.other.last_name}
+ {lessonData.music_session.pretty_scheduled_start_with_timezone} + {lessonData.displayStatus} + {unreadMessages} + menu
+ ` + + classes.push(lesson) + if classes.length == 0 + classes = [` + No Lessons Yet + `] + + `
+
+

my lessons

+
+ + + + + + + + + + + + {classes} + +
{this.otherRole()}DATE/TIMESTATUSACTIONS
+ + +
+ {searchTeachers} +
+
+
+

learn about jamclass

+ + {learnMoreAboutJamClass} + +
+ {learnMoreLink} +
+
+ {signupTestDrive} +
+

get ready for your first lesson

+ +

Be sure to set up and test the JamKazam app in an online music session a few days before + your first lesson! We're happy to help, and we'll even get in a session with you to make sure everything + is working properly. Ping us at support@jamkazam.com anytime, and + read our + JamClass user guide to learn how to use all the lesson features. +

+
+
+
+
` + + + viewerStudent: () -> + !@viewerTeacher() + + viewerTeacher: () -> + this.state.user?.is_a_teacher + + myRole: () -> + if @viewerStudent() + 'student' + else + 'teacher' + otherRole: () -> + if @viewerStudent() + 'teacher' + else + 'student' +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/JamClassStudentScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamClassStudentScreen.js.jsx.coffee deleted file mode 100644 index 8fef2d1fa..000000000 --- a/web/app/assets/javascripts/react-components/JamClassStudentScreen.js.jsx.coffee +++ /dev/null @@ -1,154 +0,0 @@ -context = window -rest = context.JK.Rest() -logger = context.JK.logger - -UserStore = context.UserStore - -@JamClassStudentScreen = React.createClass({ - - mixins: [ - @ICheckMixin, - Reflux.listenTo(AppStore, "onAppInit"), - Reflux.listenTo(UserStore, "onUserChanged") - ] - - onAppInit: (@app) -> - @app.bindScreen('jamclass', - {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide}) - - onUserChanged: (userState) -> - @setState({user: userState?.user}) - - componentDidMount: () -> - - componentDidUpdate: () -> - - getInitialState: () -> - { - user: null, - } - - beforeHide: (e) -> - - - beforeShow: (e) -> - - afterShow: (e) -> - @setState({updating: true}) - rest.getLessonSessions().done((response) => @jamClassLoaded(response)).fail((jqXHR) => @failedJamClassLoad(jqXHR)) - - resetState: () -> - @setState({updating: false, lesson: null}) - - jamClassLoaded: (response) -> - @setState({updating: false}) - @setState({summary: response}) - - failedJamClassLoad: (jqXHR) -> - @setState({updating: false}) - @setState({summary: response}) - if jqXHR.status == 404 - @app.layout.notify({title: "Unable to load JamClass info", text: "Try refreshing the web page"}) - - render: () -> - disabled = @state.updating - - classes = [] - if @state.updating - classes = [`Loading...`] - else - - `
-
-
-

my lessons

- - - - - - - - - - - - {classes} - -
TEACHERDATE/TIMESTATUSACTIONS
- - -
-
-

search teachers

- -

JamClass instructors are each individually screened to ensure that they are highly qualified music - teachers, - equipped to teach effectively online, and background checked. -

- - -
-
-
-
-

learn about jamclass

- -

- JamClass is the best way to make music lessons, offering significant advantadges over both traditional - face-to-face lessons - and online skype lessons. -

- - -
-
-

sign up for testdrive

- -

- There are two awesome, painless ways to get started with JamClass. -

- -

- Sign up for TestDrive and take 4 full 30-minute lessons - one each from 4 different instructors - for just - $49.99. - You wouldn't marry the first person you date, right? Find the best teacher for you. It's the most important - factor in the success for your lessons! -

- -

- Or take one JamClass lesson free. It's on us! We're confident you'll take more. -

- -

- Sign up for TestDrive using the button below, or to take one free lesson, search our teachers, and click the - Book Free Lesson on your favorite. -

- - -
-
-

get ready for your first lesson

- -

Be sure to set up and test the JamKazam app in an online music session a few days before - your first lesson! We're happy to help, and we'll even get in a session with you to make sure everything - is working properly. Ping us at support@jamkazam.com anytime, and - read our - JamClass user guide to learn how to use all the lesson features. -

-
-
-
-
` - -}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee b/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee index 3500dfffc..bca5cb162 100644 --- a/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee @@ -21,6 +21,9 @@ UserStore = context.UserStore onSlotDecision: (slot_decision) -> @setState({slot_decision: slot_decision?.slot_decision}) + onUpdateAllDecision: (update_all) -> + @setState({update_all: update_all?.update_all}) + componentWillMount: () -> componentDidMount: () -> @@ -75,7 +78,15 @@ UserStore = context.UserStore id: e.id, }).done((response) => @getLessonBookingDone(response)).fail(@app.ajaxError) + hasFocusedLesson: () -> + this.state.booking.focused_lesson?.id? + + focusedLesson: () -> + this.state?.booking?.focused_lesson + updateBookingState: (booking) -> + console.log("updating booking state", booking) + if booking.counter_slot? startSlotDecision = booking.counter_slot.id else @@ -84,7 +95,9 @@ UserStore = context.UserStore else startSlotDecision = booking.default_slot.id - @setState({booking: booking, updating: false, slot_decision: startSlotDecision, updatingLesson: false}) + update_all = !booking.focused_lesson?.id? + + @setState({booking: booking, updating: false, slot_decision: startSlotDecision, updatingLesson: false, update_all: update_all}) getLessonBookingDone: (response) -> @updateBookingState(response) @@ -105,13 +118,13 @@ UserStore = context.UserStore request.id = this.state.booking.id request.timezone = window.jstz.determine().name() request.message = @getMessage() - request.update_all = true + request.lesson_session_id = @focusedLesson()?.id rest.counterLessonBooking(request).done((response) => @counterLessonBookingDone(response)).fail((response) => @counterLessonBookingFail(response)) else if @state.slot_decision == 'decline' request = {} request.message = @getMessage() request.id = this.state.booking.id - request.update_all = true + request.lesson_session_id = @focusedLesson()?.id rest.cancelLessonBooking(request).done((response) => @cancelLessonBookingDone(response)).fail((response) => @cancelLessonBookingFail(response)) else if @state.slot_decision request = {} @@ -187,6 +200,7 @@ UserStore = context.UserStore minute = $slot.find('.minute').val() am_pm = $slot.find('.am_pm').val() + update_all = $slot.find('input.update-all').is(':checked') && @isRecurring() if hour? and hour != '' hour = new Number(hour) @@ -208,7 +222,7 @@ UserStore = context.UserStore day_of_week = $slot.find('.day_of_week').val() - {hour: hour, minute: minute, date: date, day_of_week: day_of_week} + {hour: hour, minute: minute, date: date, day_of_week: day_of_week, update_all: update_all} student: () -> @state.booking?.user @@ -323,9 +337,16 @@ UserStore = context.UserStore lessonType = "single #{this.lessonLength()}-minute lesson" bookedPrice: () -> - this.state.booking?.booked_price + price = this.state.booking?.booked_price + if price? + if typeof price == "string" + price = new Number(price) + return price.toFixed(2) + else + return price lessonPaymentAmt: () -> + console.log("lessonPaymentAmt") if @state.booking?.payment_style == 'elsewhere' '$10' else if @state.booking?.payment_style == 'single' @@ -333,7 +354,7 @@ UserStore = context.UserStore else if @state.booking?.payment_style == 'weekly' "at $#{this.bookedPrice()} per lesson" else if @state.booking?.payment_style == 'monthly' - "monthly at $#{this.bookedPrice()} per month" + "monthly at $#{this.bookedPrice()} per month" else "$???" @@ -400,11 +421,18 @@ UserStore = context.UserStore "#{context.JK.padString(hour.toString(), 2)}:#{context.JK.padString(slot.minute.toString(), 2)}#{am_pm} (#{slot.timezone})" + displayableLesson: () -> + lesson = @focusedLesson() + + if !lesson? + lesson = @state.booking.next_lesson + + lesson isStartingSoonOrNow: () -> # 60 minutes before - startTime = new Date(@state.booking.next_lesson.scheduled_start).getTime() + startTime = new Date(@displayableLesson().scheduled_start).getTime() endTime = (startTime + @lessonLength() * 60) now = new Date().getTime() @@ -412,17 +440,17 @@ UserStore = context.UserStore isNow: () -> - startTime = new Date(@state.booking.next_lesson.scheduled_start).getTime() + startTime = new Date(@displayableLesson().scheduled_start).getTime() endTime = (startTime + @lessonLength() * 60) now = new Date().getTime() now > startTime && now < endTime isPast: () -> - new Date().getTime() > new Date(@state.booking.next_lesson.scheduled_start).getTime() + new Date().getTime() > new Date(@displayableLesson().scheduled_start).getTime() sessionLink: () -> - link = "/client#/session/#{this.stateb.booking.next_lesson.music_session_id}" + link = "/client#/session/#{this.displayableLesson().music_session_id}" `JOIN SESSION` @@ -439,9 +467,9 @@ UserStore = context.UserStore else if @isPast() data =`

This lesson is over.

` else - data = `

This lesson is scheduled to start at {this.state.booking.next_lesson.pretty_scheduled_start}

` + data = `

This lesson is scheduled to start at {this.displayableLesson().pretty_scheduled_start}

` else if @isRequested() - data = `

This lesson is scheduled to start at {this.state.booking.next_lesson.pretty_scheduled_start}

` + data = `

This lesson is scheduled to start at {this.displayableLesson().pretty_scheduled_start}

` `
{data}
` @@ -452,9 +480,9 @@ UserStore = context.UserStore else if @isPast() `

This lesson is over.

` else - `

This lesson is scheduled to start at {this.state.booking.next_lesson.pretty_scheduled_start}

` + `

This lesson is scheduled to start at {this.displayableLesson().pretty_scheduled_start}

` else if @isRequested() - `

This lesson is scheduled to start at {this.state.booking.next_lesson.pretty_scheduled_start}

` + `

This lesson is scheduled to start at {this.displayableLesson().pretty_scheduled_start}

` renderCancelLesson: () -> `
@@ -490,10 +518,12 @@ UserStore = context.UserStore decisionProps: (slots) -> { onSlotDecision: this.onSlotDecision, + onUpdateAllDecision: this.onUpdateAllDecision, initial: this.neverAccepted(), counter: this.isCounter(), is_recurring: this.isRecurring(), slot_decision: this.state.slot_decision, + update_all: this.state.update_all, slots: slots, otherRole: this.otherRole(), onUserDecision: this.onAccept, @@ -501,18 +531,26 @@ UserStore = context.UserStore disabled: this.state.updatingLesson, selfLastToAct: this.selfLastToAct(), counterErrors: this.state.counterErrors, - cancelErrors: this.state.cancelErrors + cancelErrors: this.state.cancelErrors, + focusedLesson: this.focusedLesson() } render: () -> if @state.updating - @renderLoading() + + return @renderLoading() + else if @teacherViewing() - @renderTeacher() + + return @renderTeacher() + else if @studentViewing() - @renderStudent() + + return @renderStudent() + else - @renderLoading() + + return @renderLoading() renderTeacher: () -> @@ -546,7 +584,7 @@ UserStore = context.UserStore header = 'This lesson has been suspended' content = @renderTeacherSuspended() - `
+ return `
` renderLoading: () -> - header = 'Loading...' + header = 'Loading ...' `
diff --git a/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee b/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee index b166a9404..d427e92f6 100644 --- a/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee @@ -51,7 +51,9 @@ UserStore = context.UserStore lesson: null, updating: false, billingInUS: true, - userWantsUpdateCC: false + userWantsUpdateCC: false, + "test-drive": false, + teacher: null } beforeHide: (e) -> @@ -62,8 +64,51 @@ UserStore = context.UserStore afterShow: (e) -> @resetState() @resetErrors() - @setState({updating: true}) - rest.getUnprocessedLessonOrIntent().done((response) => @unprocessLoaded(response)).fail((jqXHR) => @failedBooking(jqXHR)) + parsed = @parseId(e.id) + parsed.updating = false + + if parsed['lesson-booking'] + parsed.updating = true + rest.getLessonBooking({id: parsed.lesson_booking_id}).done((response) => @lessonBookingLoaded(response)).fail((jqXHR) => @failedLessonBooking(jqXHR)) + else if parsed['teacher-intent'] + parsed.updating = true + rest.getUserDetail({id: parsed.teacher_id}).done((response) => @teacherLoaded(response)).fail((jqXHR) => @failedTeacher(jqXHR)) + else if parsed['test-drive'] + logger.debug("test-drive lesson payment; no teacher/booking in context") + else + logger.error("unknown state for lesson-payment") + window.location.href = '/client#/jamclass' + + @setState(parsed) + + parseId: (id) -> + result = {} + + # id can be: + # 'test-drive' + # or 'lesson-booking_id' + # or 'teacher_id + + result['test-drive'] = false + result['lesson-booking'] = false + result['teacher-intent'] = false + + bits = id.split('_') + if bits.length == 1 + # should be id=test-drive + result[id] = true + else if bits.length == 2 + type = bits[0] + if type == 'lesson-booking' + result[type] = true + result.lesson_booking_id = bits[1] + else if type == 'teacher' + result['teacher-intent'] = true + result.teacher_id = bits[1] + + logger.debug("LessonPayment: parseId " + JSON.stringify(result)) + + result resetErrors: () -> @setState({ccError: null, cvvError: null, expiryError: null, billingInUSError: null, zipCodeError: null, nameError: null}) @@ -74,25 +119,34 @@ UserStore = context.UserStore @setState({billingInUS: checked}) resetState: () -> - @setState({updating: false, lesson: null}) + @setState({updating: false, lesson: null, teacher: null, "test-drive": false, "lesson-booking" : false, "teacher-intent": false}) - unprocessLoaded: (response) -> + lessonBookingLoaded: (response) -> @setState({updating: false}) - logger.debug("unprocessed loaded", response) - @setState(response) + logger.debug("lesson booking loaded", response) - failedBooking: (jqXHR) -> - @setState({updating: false}) - @setState({lesson: null}) - if jqXHR.status == 404 - # no unprocessed lessons. That's arguably OK; the user is just going to enter their info up front. - console.log("nothing") + if response.card_presumed_ok + context.JK.Banner.showNotice("Lesson Already Requested", "You have already requested this lesson from this teacher.") + window.location.href = "/client#/jamclass" + @setState({lesson: response, teacher: response.teacher}) - failedUnprocessLoad: (jqXHR) -> + failedLessonBooking: (jqXHR) -> @setState({updating: false}) @app.layout.notify({ - title: 'Unable to load lesson', - text: 'Please attempt to book a free lesson first or refresh this page.' + title: 'unable to load lesson info', + text: 'Something has gone wrong. Please try refreshing the page.' + }) + + teacherLoaded: (response) -> + @setState({updating: false}) + logger.debug("teacher loaded", response) + @setState({teacher: response}) + + failedTeacher: (jqXHR) -> + @setState({updating: false}) + @app.layout.notify({ + title: 'unable to load teacher info', + text: 'Something has gone wrong. Please try refreshing the page.' }) onBack: (e) -> @@ -110,7 +164,7 @@ UserStore = context.UserStore text: "Please refresh this page and try to enter your info again. Sorry for the inconvenience!" }) else - if !@state.userWantsUpdateCC + if @reuseStoredCard() @attemptPurchase(null) else @@ -176,7 +230,7 @@ UserStore = context.UserStore window.Stripe.card.createToken(data, (status, response) => (@stripeResponseHandler(status, response))); stripeResponseHandler: (status, response) -> - console.log("response", response) + console.log("stripe response", JSON.stringify(response)) if response.error if response.error.code == "invalid_number" @@ -188,6 +242,12 @@ UserStore = context.UserStore else @attemptPurchase(response.id) + isNormal: () -> + @state.lesson?.lesson_type == 'paid' + + isTestDrive: () -> + @state['test-drive'] == true || @state.lesson?.lesson_type == 'test-drive' || @state['teacher-intent'] + attemptPurchase: (token) -> if this.state.billingInUS zip = @root.find('input.zip').val() @@ -195,7 +255,9 @@ UserStore = context.UserStore data = { token: token, zip: zip, - test_drive: @state.lesson?.lesson_type == 'test-drive' || (@state.intent?.intent == 'book-test-drive') + test_drive: @isTestDrive(), + booking_id: @state.lesson?.id, + normal: @isNormal() } if @state.shouldShowName @@ -204,24 +266,25 @@ UserStore = context.UserStore rest.submitStripe(data).done((response) => @stripeSubmitted(response)).fail((jqXHR) => @stripeSubmitFailure(jqXHR)) stripeSubmitted: (response) -> - logger.debug("stripe submitted", response) + logger.debug("stripe submitted: " + JSON.stringify(response)) if @state.shouldShowName window.UserActions.refresh() # if the response has a lesson, take them there if response.lesson?.id? - context.Banner.showNotice({ - title: "Lesson Requested", - text: "The teacher has been notified of your lesson request, and should respond soon.

We've taken you automatically to the page for this request, and sent an email to you with a link here as well. All communication with the teacher will show up on this page and in email." - }) - window.location = "/client#/jamclass/lesson-session/" + response.lesson.id - else if response.test_drive? || response.intent_book_test_drive? + context.JK.Banner.showNotice("Lesson Requested","The teacher has been notified of your lesson request, and should respond soon.

We've taken you automatically to the page for this request, and sent an email to you with a link here as well. All communication with the teacher will show up on this page and in email.") + + url = "/client#/jamclass/lesson-booking/" + response.lesson.id + url = "/client#/jamclass" + window.location.href = url + + else if response.test_drive? if response.test_drive?.teacher_id - teacher_id = response.test_drive?.teacher_id - else if response.intent_book_test_drive? - teacher_id = response.intent_book_test_drive?.teacher_id + teacher_id = response.test_drive.teacher_id + else if @state.teacher?.id + teacher_id = @state.teacher.id if teacher_id? text = "You now have 4 lessons that you can take with 4 different teachers.

We've taken you automatically to the lesson booking screen for the teacher you initially showed interest in." @@ -230,10 +293,7 @@ UserStore = context.UserStore else text = "You now have 4 lessons that you can take with 4 different teachers.

We've taken you automatically to the Teacher Search screen, so you can search for teachers right for you." location = "/client#/teachers/search" - context.JK.Banner.showNotice({ - title: "Test Drive Purchased", - text: text - }) + context.JK.Banner.showNotice("Test Drive Purchased",text) window.location = location else window.location = "/client#/teachers/search" @@ -263,6 +323,15 @@ UserStore = context.UserStore reuseStoredCard: () -> !@state.userWantsUpdateCC && @state.user?['has_stored_credit_card?'] + bookedPrice: () -> + booked_price = this.state.lesson.booked_price + + if booked_price? + if typeof booked_price == "string" + booked_price = new Number(booked_price) + return booked_price.toFixed(2) + else + return '??' render: () -> disabled = @state.updating || @reuseStoredCard() @@ -276,45 +345,31 @@ UserStore = context.UserStore {name}
` else - if @state.lesson? || @state.intent? - if @state.lesson? - photo_url = @state.lesson.teacher.photo_url - name = @state.lesson.teacher.name - lesson_length = this.state.lesson.lesson_length - lesson_type = this.state.lesson.lesson_type - else - photo_url = @state.intent.teacher.photo_url - name = @state.intent.teacher.name - lesson_length = 30 - lesson_type = @state.intent.intent.substring('book-'.length) - - if !photo_url? - photo_url = '/assets/shared/avatar_generic.png' - teacherDetails = `
-
- -
- {name} -
` + if @state.lesson? || @state['test-drive'] || @state.teacher? + if @state.teacher? + photo_url = @state.teacher.photo_url + name = @state.teacher.name - if lesson_type == 'single-free' - header = `

enter card info

+ if !photo_url? + photo_url = '/assets/shared/avatar_generic.png' -
Your card wil not be charged.
See explanation to the right.
+ teacherDetails = `
+
+ +
+ {name}
` - bookingInfo = `

You are booking a single free {lesson_length}-minute lesson.

` - bookingDetail = `

To book this lesson, you will need to enter your credit card information. - You will absolutely not be charged for this free lesson, and you have no further commitment to purchase - anything. We have to collect a credit card to prevent abuse by some users who would otherwise set up - multiple free accounts to get multiple free lessons. -
-

-

` - else if lesson_type == 'test-drive' + if @state.lesson? + lesson_length = @state.lesson.lesson_length + lesson_type = @state.lesson.lesson_type + else + lesson_length = 30 + lesson_type = 'test-drive' + if @isTestDrive() + if @reuseStoredCard() header = `

purchase test drive

` else @@ -329,12 +384,16 @@ UserStore = context.UserStore jamclass policies

` - else if lesson_type == 'paid' - header = `

enter payment info for lesson

` + else if @isNormal() + if @reuseStoredCard() + header = `

purchase lesson

` + else + header = `

enter payment info for lesson

` + if this.state.lesson.recurring if this.state.lesson.payment_style == 'single' bookingInfo = `

You are booking a {lesson_length} minute lesson for - ${this.state.lesson.booked_price.toFixed(2)}

` + ${this.bookedPrice()}

` bookingDetail = `

Your card will not be charged until the day of the lesson. You must cancel at least 24 hours before your lesson is scheduled, or you will be charged for the lesson in full. @@ -368,7 +427,7 @@ UserStore = context.UserStore

` else bookingInfo = `

You are booking a {lesson_length} minute lesson for - ${this.state.lesson.booked_price.toFixed(2)}

` + ${this.bookedPrice()}

` bookingDetail = `

Your card will not be charged until the day of the lesson. You must cancel at least 24 hours before your lesson is scheduled, or you will be charged for the lesson in full. @@ -378,7 +437,11 @@ UserStore = context.UserStore policies

` else - header = `

enter payment info

` + if @reuseStoredCard() + header = `

payment info already entered

` + else + header = `

enter payment info

` + bookingInfo = `

You are entering your credit card info so that later checkouts go quickly. You can skip this for now.

` bookingDetail = ` @@ -391,7 +454,7 @@ UserStore = context.UserStore

` - submitClassNames = {'button-orange': true, disabled: disabled && @state.updating} + submitClassNames = {'button-orange': true, 'purchase-btn': true, disabled: disabled && @state.updating} updateCardClassNames = {'button-grey': true, disabled: disabled && @state.updating} backClassNames = {'button-grey': true, disabled: disabled && @state.updating} diff --git a/web/app/assets/javascripts/react-components/RescheduleLessonDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/RescheduleLessonDialog.js.jsx.coffee new file mode 100644 index 000000000..daa614670 --- /dev/null +++ b/web/app/assets/javascripts/react-components/RescheduleLessonDialog.js.jsx.coffee @@ -0,0 +1,82 @@ +context = window + +@RescheduleLessonDialog = React.createClass({ + + mixins: [@PostProcessorMixin, Reflux.listenTo(@AppStore, "onAppInit")] + teacher: false + + beforeShow: (args) -> + logger.debug("RescheduleLessonDialog.beforeShow", args.d1) + + + @setState({id: args.d1, lesson_session: null}) + + rest.getLesson({id: args.d1}).done((response) => @getLessonDone(response)).fail((jqXHR) => @getLessonFail(jqXHR)) + + getLessonDone: (lesson_session) -> + @postProcessLesson(lesson_session) + @setState({lesson_session: lesson_session}) + + getLessonFail: (jqXHR) -> + @app.ajaxError(jqXHR) + + afterHide: () -> + + onAppInit: (@app) -> + dialogBindings = { + 'beforeShow': @beforeShow, + 'afterHide': @afterHide + }; + + @app.bindDialog('reschedule-lesson-dialog', dialogBindings); + + componentDidMount: () -> + @root = $(@getDOMNode()) + + getInitialState: () -> + {id: null, lesson_session: null} + + onCloseClicked: (e) -> + e.preventDefault() + @app.layout.closeDialog('reschedule-lesson-dialog'); + + lesson: () -> + this.state.lesson_session + + viewerStudent: () -> + @lesson().student.id == context.JK.currentUserId + + viewerTeacher: () -> + !@viewerStudent() + + + render: () -> + + title = 'loading...' + if this.state.lesson_session? + title = "reschedule lesson" + lessonSessionId = this.state.lesson_session.id + other = this.state.lesson_session.other + + if @viewerStudent() && @sessionStartingSoon() + title = 'Policy Issue' + content = + `
+

We’re sorry, but you cannot reschedule a lesson less than 24 hours before the lesson start time. This is not allowed in the terms of service because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost.

+
+ CLOSE +
+
` + + `
+
+ + +

{title}

+
+
+ +
+
` + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/StripeConnect.js.jsx.coffee b/web/app/assets/javascripts/react-components/StripeConnect.js.jsx.coffee index b6ef5d193..2060193f9 100644 --- a/web/app/assets/javascripts/react-components/StripeConnect.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/StripeConnect.js.jsx.coffee @@ -19,12 +19,23 @@ UserStore = context.UserStore e.preventDefault() this.setState({clicked: true}) - StripeActions.connect(this.props.purpose) + StripeActions.connect(this.props.purpose, this.props.user) render: () -> + if this.props.purpose == 'jamclass-home' + label = `` + else + label = `` + if this.props.user?.stripe_auth? - return `
You have successfully connected your Stripe account for payments. If you need to make any changes to your Stripe account, please go to the Stripe website and sign in using your Stripe credentials there to make any changes needed.
` + if this.props.purpose == 'jamclass-home' + return `
+

Your Stripe account is properly set up and connected to enable transfer of student payments. To view lesson payment history, click the button below.

+ VIEW PAYMENTS +
` + else + return `
You have successfully connected your Stripe account for payments. If you need to make any changes to your Stripe account, please go to the Stripe website and sign in using your Stripe credentials there to make any changes needed.
` if this.state.clicked imageUrl = '/assets/content/stripe-connect-light-on-dark.png' @@ -32,7 +43,8 @@ UserStore = context.UserStore imageUrl = '/assets/content/stripe-connect-blue-on-dark.png' `
- + {label} +
` 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 2db743425..0a2273705 100644 --- a/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee @@ -139,9 +139,24 @@ ProfileActions = @ProfileActions @app.ajaxError(jqXHR, textStatus, errorMessage) ) - bookNormalLesson: (e) -> + bookNormalLesson: (user, e) -> e.preventDefault() + rest.getTestDriveStatus({id: context.JK.currentUserId, teacher_id: user.id}) + .done((response) => + if response.remaining_test_drives > 0 + buttons = [] + buttons.push({name: 'CANCEL', buttonStyle: 'button-grey'}) + buttons.push({name: 'PAY NORMALLY', buttonStyle: 'button-orange', click:(() => ( window.location.herf = '/client#/jamclass/book-lesson/normal_' + user.id ))}) + buttons.push({name: 'USE TEST DRIVE', buttonStyle: 'button-orange', click:(() => ( window.location.herf = '/client#/jamclass/book-lesson/test-drive_' + user.id ))}) + context.JK.Banner.show({title: 'You still have TestDrive Credits!', html: "You have #{response.remaining_test_drives} TestDrive credits remaining. Would you rather use your TestDrive credit for a lesson with this teacher?", buttons: buttons}) + else + window.location.href = '/client#/jamclass/book-lesson/normal_' + user.id + ) + .fail((jqXHR, textStatus, errorMessage) => + @app.ajaxError(jqXHR, textStatus, errorMessage) + ) + bookFreeLesson: (e) -> e.preventDefault() @@ -215,7 +230,7 @@ ProfileActions = @ProfileActions if !bio? bio = 'No bio' - resultsJsx.push(`
+ resultsJsx.push(`

diff --git a/web/app/assets/javascripts/react-components/TryTestDriveDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/TryTestDriveDialog.js.jsx.coffee index aff9f15fa..bc541ed9d 100644 --- a/web/app/assets/javascripts/react-components/TryTestDriveDialog.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TryTestDriveDialog.js.jsx.coffee @@ -35,7 +35,7 @@ ConfigureTracksActions = @ConfigureTracksActions marked: () -> @app.layout.closeDialog('try-test-drive') - window.location.href = "/client#/jamclass/lesson-payment" + window.location.href = "/client#/jamclass/lesson-payment/test-drive" failedMark: (jqXHR, textStatus, errorMessage) -> @app.ajaxError(jqXHR, textStatus, errorMessage) @@ -64,7 +64,7 @@ ConfigureTracksActions = @ConfigureTracksActions
` diff --git a/web/app/assets/javascripts/react-components/actions/ChatActions.js.coffee b/web/app/assets/javascripts/react-components/actions/ChatActions.js.coffee index 4b05d8d78..4757d6488 100644 --- a/web/app/assets/javascripts/react-components/actions/ChatActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/ChatActions.js.coffee @@ -8,4 +8,5 @@ context = window sessionStarted: {} activateChannel: {} fullyOpened: {} + initializeLesson: {} }) diff --git a/web/app/assets/javascripts/react-components/landing/SchoolStudentLandingPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/SchoolStudentLandingPage.js.jsx.coffee index dadf5ceee..5203db554 100644 --- a/web/app/assets/javascripts/react-components/landing/SchoolStudentLandingPage.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/landing/SchoolStudentLandingPage.js.jsx.coffee @@ -39,10 +39,10 @@ rest = context.JK.Rest() {errorText}
-
+
-
+
diff --git a/web/app/assets/javascripts/react-components/landing/SchoolTeacherLandingPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/SchoolTeacherLandingPage.js.jsx.coffee index 7f6292fad..7fbf2bb40 100644 --- a/web/app/assets/javascripts/react-components/landing/SchoolTeacherLandingPage.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/landing/SchoolTeacherLandingPage.js.jsx.coffee @@ -39,10 +39,10 @@ rest = context.JK.Rest() {errorText}
-
+
-
+
diff --git a/web/app/assets/javascripts/react-components/mixins/ICheckMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/ICheckMixin.js.coffee index 3ab17fcca..2964025de 100644 --- a/web/app/assets/javascripts/react-components/mixins/ICheckMixin.js.coffee +++ b/web/app/assets/javascripts/react-components/mixins/ICheckMixin.js.coffee @@ -13,8 +13,11 @@ teacherActions = window.JK.Actions.Teacher setCheckboxState: () -> for checkbox in @checkboxes selector = checkbox.selector - stateKey = checkbox.stateKey - choice = @state[stateKey] + + if checkbox.stateKey? + choice = @state[checkbox.stateKey] + else + choice = @props[checkbox.propsKey] $candidate = @root.find(selector) @@ -37,7 +40,6 @@ teacherActions = window.JK.Actions.Teacher if checkBoxes.length > 0 context.JK.checkbox(checkBoxes) checkBoxes.on('ifChanged', (e) => @checkIfCanFire(e)) - radioBoxes = @root.find('input[type="radio"]') if radioBoxes.length > 0 context.JK.checkbox(radioBoxes) diff --git a/web/app/assets/javascripts/react-components/mixins/PostProcessorMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/PostProcessorMixin.js.coffee new file mode 100644 index 000000000..1c77ab5d1 --- /dev/null +++ b/web/app/assets/javascripts/react-components/mixins/PostProcessorMixin.js.coffee @@ -0,0 +1,59 @@ +context = window +teacherActions = window.JK.Actions.Teacher + +@PostProcessorMixin = { + + postProcessLesson: (lesson) -> + + if lesson.music_session.user_id == context.JK.currentUserId + me = lesson.student + other = lesson.teacher + lesson.hasUnreadMessages = lesson['student_unread_messages'] + else + me = lesson.teacher + other = lesson.student + lesson.hasUnreadMessages = lesson['teacher_unread_messages'] + + lesson.me = me + lesson.other = other + lesson.isAdmin = context.JK.currentUserAdmin + lesson.cardNotOk = !lesson.lesson_booking.card_presumed_ok + + console.log("lesson.isAdmin",lesson.isAdmin ) + if (lesson.status == 'requested' || lesson.status == 'countered') + lesson.isRequested = true + if lesson.cardNotOk + lesson.displayStatus = 'No Card' + else + lesson.displayStatus = 'Requested' + if lesson['is_active?'] && (lesson.status == 'approved' ) + lesson.isScheduled = true + lesson.displayStatus = 'Scheduled' + + if !lesson.displayStatus? + if lesson.status == 'canceled' + lesson.displayStatus = 'Canceled' + else if lesson.status == 'suspended' + lesson.displayStatus = 'Suspended' + else + if lesson.success + lesson.displayStatus = 'Completed' + else + lesson.displayStatus = 'Missed' + + @postProcessUser(me) + @postProcessUser(other) + + postProcessUser: (user) -> + if !user.photo_url? + user.resolved_photo_url = '/assets/shared/avatar_generic.png' + else + user.resolved_photo_url = user.photo_url + if user.teacher? + user.teacher_profile = '/client#/profile/teacher/' + user.id + user.best_profile = user.teacher_profile + else + user.musician_profile = '/client#/profile/' + user.id + user.best_profile = user.musician_profile + +} \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/stores/ChatStore.js.coffee b/web/app/assets/javascripts/react-components/stores/ChatStore.js.coffee index 35f09e286..c6f175967 100644 --- a/web/app/assets/javascripts/react-components/stores/ChatStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/ChatStore.js.coffee @@ -52,21 +52,37 @@ SessionStore = context.SessionStore @fetchHistory() @onEmptyChannel(@channel) - buildQuery: () -> - query = {type: 'CHAT_MESSAGE', limit:@limit, page: @currentPage, channel: @channel}; + buildQuery: (channel = null) -> + if !channel? + channel = @channel - if @channel == 'session' && SessionStore.currentSessionId? + + query = {type: 'CHAT_MESSAGE', limit:@limit, page: @currentPage, channel: channel}; + + if channel == 'session' && SessionStore.currentSessionId? query.music_session = SessionStore.currentSessionId + if channel == 'lesson' && @lessonSessionId? + query.lesson_session = @lessonSessionId + if @next query.start = next return query - fetchHistory: () -> + onInitializeLesson: (lessonSessionId) -> + @lessonSessionId = lessonSessionId + + @fetchHistory('lesson') + + fetchHistory: (channel = null) -> + + if !channel? + channel = @channel + # load previous chat messages - rest.getChatMessages(@buildQuery()) + rest.getChatMessages(@buildQuery(channel)) .done((response) => - @onLoadMessages(@channel, response) + @onLoadMessages(channel, response) ).fail((jqXHR) => @app.notifyServerError(jqXHR, 'Unable to Load Session Conversations') ) @@ -85,11 +101,16 @@ SessionStore = context.SessionStore convert.msg_id = chat.id convert.created_at = chat.created_at convert.channel = chat.channel + convert.purpose = chat.purpose converted.push(convert) converted # called from ChatPanel onLoadMessages: (channel, msgs) -> + + if channel == 'lesson' + channel = @lessonSessionId + channelMsgs = @msgs[channel] if !channelMsgs? @@ -120,27 +141,36 @@ SessionStore = context.SessionStore onMsgReceived: (msg) -> - channelMsgs = @msgs[msg.channel] + effectiveChannel = msg.channel + + if msg.channel == 'lesson' + effectiveChannel = msg.lesson_session_id + + console.log("effective channel", effectiveChannel, @msgs) + channelMsgs = @msgs[effectiveChannel] if !channelMsgs? channelMsgs = [] - @msgs[msg.channel] = channelMsgs + @msgs[effectiveChannel] = channelMsgs channelMsgs.push(msg) # don't let the global channel grow indefinitely - if msg.channel == 'global' + if effectiveChannel == 'global' while channelMsgs.length > @max_global_msgs channelMsgs.shift() @changed() - buildMessage:(msg) -> + buildMessage:(msg, target_user, channel) -> payload = {message: msg} - if @channel == 'session' + if channel == 'session' payload.music_session = SessionStore.currentSessionId - payload.channel = @channel + else if channel == 'lesson' + payload.lesson_session = @lessonSessionId + payload.channel = channel payload.client_id = @app.clientId + payload.target_user = target_user payload onActivateChannel: (channel) -> @@ -148,8 +178,10 @@ SessionStore = context.SessionStore @fetchHistory() @changed() - onSendMsg: (msg, done, fail) -> - rest.createChatMessage(@buildMessage(msg)) + onSendMsg: (msg, done, fail, target_user = null, channel = null) -> + if !channel? + channel = @channel + rest.createChatMessage(@buildMessage(msg, target_user, channel)) .done((response) => done(response) @@ -163,16 +195,27 @@ SessionStore = context.SessionStore created_at: response.created_at, channel: response.channel }) + else if response.channel == 'lesson' + @onMsgReceived({ + sender_name: "me", + sender_id: context.JK.currentUserId, + msg: msg, + msg_id: response.id, + created_at: response.created_at, + channel: response.lesson_session_id + }) ) .fail((jqXHR) => fail(jqXHR) ) # unused/untested. send direct to gateway - onSendMsgInstant: (msg) -> + onSendMsgInstant: (msg, channel = null) -> logger.debug("ChatStore.sendMsg", msg) - window.JK.JamServer.sendChatMessage(@channel, msg) + if !channel? + channel = @channel + window.JK.JamServer.sendChatMessage(channel, msg) getState: () -> return {msgs: @msgs, channel: @channel} diff --git a/web/app/assets/javascripts/react-components/stores/StripeStore.js.coffee b/web/app/assets/javascripts/react-components/stores/StripeStore.js.coffee index 5e6a26e6c..e8398299e 100644 --- a/web/app/assets/javascripts/react-components/stores/StripeStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/StripeStore.js.coffee @@ -13,16 +13,22 @@ rest = new context.JK.Rest() onAppInit: (@app) -> - onConnect: (purpose) -> + onConnect: (purpose, user) -> if purpose == 'school' redirect = '/client#/account/school' + else if purpose == 'jamclass-home' + redirect = '/client#/jamclass' else - raise "no purpose #{purpose}" + throw "unknown purpose #{purpose}" - rest.createRedirectHint({redirect_location:redirect}).done((response) => @onHintDone(response)).fail(@app.ajaxError) + rest.createRedirectHint({redirect_location:redirect}).done((response) => @onHintDone(response, user)).fail(@app.ajaxError) - onHintDone: () -> + onHintDone: (response, user) -> redirectUri = encodeURIComponent(context.JK.makeAbsolute('/auth/stripe_connect/callback')) - window.location.href = "/auth/stripe_connect?redirect_uri=#{redirectUri}&scope=read_write" + teacherProfileUri = encodeURIComponent(context.JK.makeAbsolute("/client#/profile/teacher/#{user.id}")) + email = encodeURIComponent(user.email) + firstName = encodeURIComponent(user.first_name) + lastName = encodeURIComponent(user.last_name) + window.location.href = "/auth/stripe_connect?redirect_uri=#{redirectUri}&scope=read_write&stripe_user[url]=#{teacherProfileUri}&stripe_user[email]=#{email}&stripe_user[first_name]=#{firstName}&stripe_user[last_name]=#{lastName}" } ) diff --git a/web/app/assets/stylesheets/client/accountPaymentHistory.css.scss b/web/app/assets/stylesheets/client/accountPaymentHistory.css.scss index 2ef59a48c..0a01d1829 100644 --- a/web/app/assets/stylesheets/client/accountPaymentHistory.css.scss +++ b/web/app/assets/stylesheets/client/accountPaymentHistory.css.scss @@ -3,8 +3,24 @@ #account-payment-history { .content-body-scroller { - padding:20px; + height:100%; @include border_box_sizing; + overflow:auto; + } + + .content-body { + padding-top:29px; + height:100%; + width:100%; + @include border_box_sizing; + } + + div[data-react-class="AccountPaymentHistoryScreen"] { + height: 100%; + } + + .profile-header { + padding: 10px 40px !important; } table td.loading { @@ -23,17 +39,11 @@ } } - .account-left { + .account-header { float: left; - min-width: 165px; - width: 20%; - } - - .account-left h2 { - color: #FFFFFF; - font-size: 23px; - font-weight: 400; - margin-bottom: 20px; + padding-top: 43px; + font-size: 20px; + font-weight: bold; } .input-aligner { @@ -41,9 +51,70 @@ text-align:right; .back { - margin-right:22px; + margin-right:7px; } } + .profile-nav { + + &.student { + width:70%; + } + &.teacher { + width:84%; + } + + } + .profile-nav a { + position: absolute; + text-align: center; + bottom:0; + left:0; + width: 100%; + margin: 0 auto; + padding: 0 10px 0; + color:white; + background-color:transparent; + @include border-box_sizing; + } + + .profile-tile { + + float: left; + @include border-box_sizing; + height: 90px; + position: relative; + background-color:#535353; + + &.student + { + width: 50%; + } + &.teacher + { + width: 33%; + } + &.activeTile + { + background-color: #ed3618; + } + } + .profile-body { + padding-top: 100px; + } + .profile-nav { + margin: 0; + width: 84%; + } + .profile-wrapper { + padding: 10px 20px + } + + .main-content { + float: left; + @include border-box_sizing; + width: 100%; + } + } \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css index f7a854e58..380fd67dd 100644 --- a/web/app/assets/stylesheets/client/client.css +++ b/web/app/assets/stylesheets/client/client.css @@ -55,6 +55,7 @@ *= require ./iconInstrumentSelect *= require ./muteSelect *= require ./manageVsts + *= require ./lessonSessionActions *= require ./vstEffects *= require ./metronomePlaybackModeSelect *= require ./terms diff --git a/web/app/assets/stylesheets/client/jamkazam.css.scss b/web/app/assets/stylesheets/client/jamkazam.css.scss index f54c59b1b..5905958f1 100644 --- a/web/app/assets/stylesheets/client/jamkazam.css.scss +++ b/web/app/assets/stylesheets/client/jamkazam.css.scss @@ -725,4 +725,12 @@ $ReactSelectVerticalPadding: 3px; #ui-datepicker-div { z-index:10 !important; +} + +button.stripe-connect { + background-color: transparent; + padding: 0; + border: 0; + margin:0 auto; + display:block; } \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/lessonSessionActions.css.scss b/web/app/assets/stylesheets/client/lessonSessionActions.css.scss new file mode 100644 index 000000000..1564a4215 --- /dev/null +++ b/web/app/assets/stylesheets/client/lessonSessionActions.css.scss @@ -0,0 +1,40 @@ +@import "client/common"; + +.lesson-action-popup { + + &.not-card-ok .bt-content{ + height:20px !important; + } + + &.is-requested .bt-content{ + height:80px; + } + + &.is-scheduled .bt-content{ + height:122px; + } + + &.is-admin .bt-content{ + height:145px; + } + + .bt-content { + height:80px; + width:250px; + background-color:#333; + overflow:auto; + border:1px solid #ED3618; + text-align:left; + font-family: 'Raleway', Arial, Helvetica, sans-serif; + ul { + @include vertical-align-column; + height:100%; + margin-left: 0 !important; + } + li { + font-size:12px; + margin-left:0 !important; + list-style-type: none; + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/react-components/ChatWindow.css.scss b/web/app/assets/stylesheets/client/react-components/ChatWindow.css.scss index b6e5e3ce1..4cc84323a 100644 --- a/web/app/assets/stylesheets/client/react-components/ChatWindow.css.scss +++ b/web/app/assets/stylesheets/client/react-components/ChatWindow.css.scss @@ -1,11 +1,78 @@ @import "client/common"; div[data-react-class="ChatWindow"] { - height:100%; + height: 100%; +} - .ChatWindow { - height:100%; +.ChatWindow { + + &.ChatDialog { + .active-tab { + top: 0; + margin-top: 0; + } + + #new-chat-message { + height: 40px; + width: 100%; + } + + .active-tab { + padding-bottom:105px; + } + .chat-list-scroller { + margin:0; + } + .chat-sender { + margin:0; + left:0; + padding:0; + } + .chat-message { + margin:0 0 15px 0; + .chat-message-text { + margin-top:8px; + display:block; + } + .chat-message-timestamp { + display:inline-block; + } + .chat-message-sender { + &:after { + content: ''; + } + } + } + + .chat-message-form { + text-align:center; + padding: 0 25px; + } + .btn-close-chat { + float:right; + margin: 8px 15px 0 0; + width:50px; + } + + .email-notice { + color:$ColorTextTypical; + font-size:11px; + float:right; + margin: 7px 10px 0 0; + } + + .btn-send-chat-message { + margin: 8px -4px 0 0; + width:50px; + } + .chat-message-purpose { + display:inline; + color: $ColorTextTypical; + margin: 0 10px 0 0; + font-size:11px; + } } + height: 100%; .active-tab { height: 100%; @@ -13,20 +80,20 @@ div[data-react-class="ChatWindow"] { top: -24px; box-sizing: border-box; padding-bottom: 54px; - margin-top:24px; + margin-top: 24px; } .chat-tabs { - text-align:center; + text-align: center; } .chat-tab { - margin:0 2px; - display:inline-block; + margin: 0 2px; + display: inline-block; - padding:2px; + padding: 2px; color: #ED3618; - border-style:solid; - border-color:#09525b; + border-style: solid; + border-color: #09525b; a { //color:white; @@ -40,15 +107,14 @@ div[data-react-class="ChatWindow"] { &.active { a { text-decoration: none; - color:white; + color: white; } - background-color:transparent; - border-color:#09525b; - border-width:0; + background-color: transparent; + border-color: #09525b; + border-width: 0; } } - .chat-list-scroller { position: relative; display: block; @@ -62,7 +128,7 @@ div[data-react-class="ChatWindow"] { } .btn-send-chat-message { - float:right; + float: right; margin: 3px 0 0; } @@ -78,29 +144,30 @@ div[data-react-class="ChatWindow"] { } .chat-message { - margin:5px 0; + margin: 5px 0; + font-size:12px; .chat-message-sender { - font-weight:bold; - margin-right:10px; + font-weight: bold; + margin-right: 6px; color: #ED3618; &:after { - content:':' + content: ':' } } .chat-message-text { - line-height:18px; - white-space:pre-line; + line-height: 15px; + white-space: pre-line; color: #D5E2E4; } .chat-message-timestamp { - margin-top:4px; - color:#AAA; - display:block; - font-size:12px; + margin-top: 4px; + color: #AAA; + display: block; + margin-left:4px; } } @@ -114,4 +181,4 @@ div[data-react-class="ChatWindow"] { box-sizing: border-box; } -} \ No newline at end of file +} diff --git a/web/app/assets/stylesheets/client/react-components/JamClassScreen.css.scss b/web/app/assets/stylesheets/client/react-components/JamClassScreen.css.scss new file mode 100644 index 000000000..86872a706 --- /dev/null +++ b/web/app/assets/stylesheets/client/react-components/JamClassScreen.css.scss @@ -0,0 +1,190 @@ +@import "client/common"; + +#jam-class-student-screen { + + div[data-react-class="JamClassScreen"] { + height:100%; + } + .content-body-scroller { + height:100%; + padding:30px; + @include border_box_sizing; + } + + h2 { + font-size: 20px; + font-weight:700; + margin-bottom: 20px !important; + display:inline-block; + } + .column { + @include border_box_sizing; + width:50%; + } + .column-left { + float:left; + width:70%; + } + .column-right { + float:right; + width:30%; + padding-left:20px; + } + p { + line-height:125% !important; + font-size:14px !important; + margin:0 0 20px 0 !important; + color: $ColorTextTypical; + } + .avatar { + display:inline-block; + padding:1px; + width:36px; + height:36px; + background-color:#ed4818; + margin:0 10px 0 0; + -webkit-border-radius:18px; + -moz-border-radius:18px; + border-radius:18px; + float:none; + } + .avatar img { + width: 36px; + height: 36px; + -webkit-border-radius:18px; + -moz-border-radius:18px; + border-radius:18px; + } + + .calender-integration-notice { + display:block; + text-align:center; + } + .actions { + display:block; + text-align:center; + } + .jamclass-section { + margin-bottom:40px; + &.my-lessons { + max-height:300px; + overflow:auto; + } + } + .jamtable { + + height:100%; + width:100%; + table-layout:fixed; + + a { + text-decoration: underline !important; + color:#fc0 !important; + } + th { + font-size:14px; + padding:3px 10px; + @include border_box_sizing; + &.role { + text-transform: uppercase; + } + } + td { + background-color: #2b2b2b; + padding:4px 15px; + font-size:14px; + vertical-align:middle; + color: $ColorTextTypical; + @include border_box_sizing; + } + tbody { + + } + + .userColumn { + //white-space:nowrap; + .first_name { + margin-bottom:5px; + display:inline-block; + } + .last_name { + display:inline-block; + } + a { + text-decoration: none; + } + } + + .unreadColumn { + width:44px; + text-align:center; + padding:4px 10px; + } + .displayStatusColumn { + width:100px; + text-align:center; + } + .actionsColumn { + text-align:center; + width:80px; + } + .lesson-session-actions-btn { + white-space: nowrap; + cursor: pointer; + vertical-align: bottom; + text-decoration: none !important; + color:#fc0; + + .arrow-down { + float:none; + margin-left:5px; + margin-top:0; + margin-right:0; + border-top: 4px solid #fc0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + display:inline-block; + } + .arrow-up { + float:none; + margin-right:0; + margin-left:5px; + margin-bottom:2px; + border-bottom: 4px solid #fc0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + display:inline-block; + } + } + } + .search-teachers-btn { + margin-top:10px; + } + .loading, .none { + text-align:center; + } + .field.stripe-connect label { + color: $ColorTextTypical; + } + .field.stripe-connect .view-payments { + display:block; + margin: 0 auto; + width:110px; + } + .view-teacher-profile { + display:block; + width:112px; + margin: 20px auto 0; + } + a.missing-profile { + display:block; + } + span.missing-profile { + display:block; + } + .missing-profile-list { + margin: 0 auto; + display: block; + text-align: center; + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/react-components/JamClassStudentScreen.css.scss b/web/app/assets/stylesheets/client/react-components/JamClassStudentScreen.css.scss deleted file mode 100644 index d56e04dcf..000000000 --- a/web/app/assets/stylesheets/client/react-components/JamClassStudentScreen.css.scss +++ /dev/null @@ -1,86 +0,0 @@ -@import "client/common"; - -#jam-class-student-screen { - - div[data-react-class="JamClassStudentScreen"] { - height:100%; - } - .content-body-scroller { - height:100%; - padding:30px; - @include border_box_sizing; - } - - h2 { - font-size: 20px; - font-weight:700; - margin-bottom: 20px !important; - display:inline-block; - } - .column { - @include border_box_sizing; - width:50%; - } - .column-left { - float:left; - padding-right:20px; - } - .column-right { - float:right; - padding-left:20px; - } - p { - line-height:125% !important; - font-size:14px !important; - margin:0 0 20px 0 !important; - color: $ColorTextTypical; - } - .avatar { - display:inline-block; - padding:1px; - width:36px; - height:36px; - background-color:#ed4818; - margin:10px 20px 0 0; - -webkit-border-radius:18px; - -moz-border-radius:18px; - border-radius:18px; - float:none; - } - .avatar img { - width: 36px; - height: 36px; - -webkit-border-radius:18px; - -moz-border-radius:18px; - border-radius:18px; - } - - .calender-integration-notice { - display:block; - text-align:center; - } - .actions { - display:block; - text-align:center; - } - .jamclass-section { - margin-bottom:40px; - } - .jamtable { - a { - text-decoration: underline !important; - color:#fc0 !important; - } - th { - font-size:14px; - padding:3px 10px; - } - td { - padding:4px 15px; - font-size:14px; - } - tbody { - - } - } -} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/react-components/LessonBookingScreen.css.scss b/web/app/assets/stylesheets/client/react-components/LessonBookingScreen.css.scss index c52afb911..341a498e9 100644 --- a/web/app/assets/stylesheets/client/react-components/LessonBookingScreen.css.scss +++ b/web/app/assets/stylesheets/client/react-components/LessonBookingScreen.css.scss @@ -216,6 +216,12 @@ margin-right: 5px; } + .icheckbox_minimal { + display:inline-block; + top: 3px; + margin-right: 5px; + } + textarea.message { width:100%; height:150px; diff --git a/web/app/assets/stylesheets/dialogs/CancelLessonDialog.css.scss b/web/app/assets/stylesheets/dialogs/CancelLessonDialog.css.scss new file mode 100644 index 000000000..05ace54ad --- /dev/null +++ b/web/app/assets/stylesheets/dialogs/CancelLessonDialog.css.scss @@ -0,0 +1,19 @@ +@import "client/common"; + +#cancel-lesson-dialog { + width: 500px; + max-height:400px; + + h3 { + color:white; + margin-bottom:20px; + } + .dialog-inner { + width: auto; + height:100%; + } + + .ChatDialogTop { + height:100%; + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/dialogs/RescheduleLessonDialog.css.scss b/web/app/assets/stylesheets/dialogs/RescheduleLessonDialog.css.scss new file mode 100644 index 000000000..84f5df481 --- /dev/null +++ b/web/app/assets/stylesheets/dialogs/RescheduleLessonDialog.css.scss @@ -0,0 +1,19 @@ +@import "client/common"; + +#reschedule-lesson-dialog { + width: 500px; + min-height:300px; + + h3 { + color:white; + margin-bottom:20px; + } + .dialog-inner { + width: auto; + height:100%; + } + + .actions { + text-align:center; + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/dialogs/chatDialog.css.scss b/web/app/assets/stylesheets/dialogs/chatDialog.css.scss new file mode 100644 index 000000000..43378f972 --- /dev/null +++ b/web/app/assets/stylesheets/dialogs/chatDialog.css.scss @@ -0,0 +1,22 @@ +@import "client/common"; + +#chat-dialog { + width: 700px; + max-height:500px; + + h3 { + color:white; + margin-bottom:20px; + } + .dialog-inner { + width: auto; + height:calc(100% - 29px) + } + + div[data-react-class="ChatDialog"] { + height:500px; + } + .ChatDialogTop { + height:100%; + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/landings/school_landing.css.scss b/web/app/assets/stylesheets/landings/school_landing.css.scss index 51dac63e1..f7864b036 100644 --- a/web/app/assets/stylesheets/landings/school_landing.css.scss +++ b/web/app/assets/stylesheets/landings/school_landing.css.scss @@ -43,6 +43,9 @@ body.web.school_register { } } + .field { + margin-top:1px; + } .header-content { display:inline-block; } diff --git a/web/app/controllers/api_chats_controller.rb b/web/app/controllers/api_chats_controller.rb index 82cd61e90..fa0b28111 100644 --- a/web/app/controllers/api_chats_controller.rb +++ b/web/app/controllers/api_chats_controller.rb @@ -6,7 +6,7 @@ class ApiChatsController < ApiController def create - @chat_msg = ChatMessage.create(current_user, @music_session, params[:message], params[:channel], params[:client_id]) + @chat_msg = ChatMessage.create(current_user, @music_session, params[:message], params[:channel], params[:client_id], @target_user, @lesson_session) respond_with_model(@chat_msg) end @@ -30,6 +30,19 @@ class ApiChatsController < ApiController end end + if params.has_key?(:lesson_session) + @lesson_session = LessonSession.find(params[:lesson_session]) + if @lesson_session.nil? + raise ArgumentError, 'specified lesson session not found' + end + + unless @lesson_session.access? current_user + raise JamPermissionError, 'not allowed to join the specified lesson session' + end + + @target_user = User.find(params[:target_user]) if params[:target_user] + end + end end \ No newline at end of file diff --git a/web/app/controllers/api_lesson_bookings_controller.rb b/web/app/controllers/api_lesson_bookings_controller.rb index 6ad88caef..da570b372 100644 --- a/web/app/controllers/api_lesson_bookings_controller.rb +++ b/web/app/controllers/api_lesson_bookings_controller.rb @@ -149,7 +149,13 @@ class ApiLessonBookingsController < ApiController def counter - next_lesson = @lesson_booking.next_lesson + if params[:lesson_session_id] + target_lesson = @lesson_session.find(params[:lesson_session_id]) + else + target_lesson = @lesson_booking.next_lesson + end + + @lesson_session = target_lesson slot = LessonBookingSlot.new if @lesson_booking.recurring @@ -169,7 +175,7 @@ class ApiLessonBookingsController < ApiController slot.timezone = params[:timezone] slot.update_all = params[:update_all] - result = next_lesson.counter({ + result = target_lesson.counter({ proposer: current_user, message: params[:message], slot: slot @@ -182,15 +188,21 @@ class ApiLessonBookingsController < ApiController recursive_errors(result, [:lesson_booking_slots]) else raise "unknown response type in counter #{result.class}" - end + end return end @lesson_booking.reload end def cancel - next_lesson = @lesson_booking.next_lesson - result = next_lesson.cancel({ + if params[:lesson_session_id] + target_lesson = LessonSession.find(params[:lesson_session_id]) + else + target_lesson = @lesson_booking.next_lesson + end + @lesson_session = target_lesson + + result = target_lesson.cancel({ canceler: current_user, message: params[:message], update_all: true @@ -227,9 +239,9 @@ class ApiLessonBookingsController < ApiController if @lesson_booking.nil? # try with lesson session - lesson_session = LessonSession.find_by_id(params[:id]) - if lesson_session - @lesson_booking = lesson_session.lesson_booking + @lesson_session = LessonSession.find_by_id(params[:id]) + if @lesson_session + @lesson_booking = @lesson_session.lesson_booking end end raise ActiveRecord::RecordNotFound, "Can't find lesson booking" if @lesson_booking.nil? diff --git a/web/app/controllers/api_lesson_sessions_controller.rb b/web/app/controllers/api_lesson_sessions_controller.rb index a63066e71..96a73b9d3 100644 --- a/web/app/controllers/api_lesson_sessions_controller.rb +++ b/web/app/controllers/api_lesson_sessions_controller.rb @@ -10,11 +10,91 @@ class ApiLessonSessionsController < ApiController data = LessonSession.index(current_user, params) @lesson_sessions = data[:query] - + @show_teacher = true @next = data[:next_page] render "api_lesson_sessions/index", :layout => nil end + + def show + + end + + def start_time + if !current_user.admin + response = { message: 'not admin' } + render :json => response, :status => 422 + else + time = Time.zone.local_to_utc(Time.now + params[:minutes].to_i * 60) + @lesson_session.music_session.scheduled_start = time + @lesson_session.music_session.save! + render :json => {}, :status => 200 + end + end + + def update_unread_messages + + if params.has_key?(:teacher_unread_messages) + @lesson_session.teacher_unread_messages = params[:teacher_unread_messages] + end + + if params.has_key?(:student_unread_messages).present? + @lesson_session.student_unread_messages = params[:student_unread_messages] + end + + if !@lesson_session.save + respond_with_model(@lesson_session) + return + end + + end + + def reschedule_check + + # check if within 24 hours + + if params[:update_all] + # check if the next scheduled lesson is doable + if 24.hours.from_now > @lesson_session.lesson_booking.next_lesson.music_session.scheduled_start + response = { message: 'time_limit' } + render :json => response, :status => 422 + return + end + else + if 24.hours.from_now > @lesson_session.music_session.scheduled_start + response = { message: 'time_limit' } + render :json => response, :status => 422 + return + end + end + + render :json => {}, :status => 200 + end + + def cancel_check + if @lesson_session.student.id == current_user.id + # check if within 24 hours + + if params[:update_all] + # check if the next scheduled lesson is doable + if 24.hours.from_now > @lesson_session.booking.next_lesson.music_session.scheduled_start + response = { message: 'time_limit' } + render :json => response, :status => 422 + return + end + else + if 24.hours.from_now > @lesson_session.music_session.scheduled_start + response = { message: 'time_limit' } + render :json => response, :status => 422 + return + end + end + end + + render :json => {}, :status => 200 + end + + private def lookup_lesson diff --git a/web/app/controllers/api_stripe_controller.rb b/web/app/controllers/api_stripe_controller.rb index a71ce096d..9501141a5 100644 --- a/web/app/controllers/api_stripe_controller.rb +++ b/web/app/controllers/api_stripe_controller.rb @@ -14,7 +14,6 @@ class ApiStripeController < ApiController @lesson = data[:lesson] @test_drive = data[:test_drive] @normal = data[:normal] - @intent = data[:intent] end end diff --git a/web/app/controllers/api_teacher_distributions_controller.rb b/web/app/controllers/api_teacher_distributions_controller.rb new file mode 100644 index 000000000..30cb4265b --- /dev/null +++ b/web/app/controllers/api_teacher_distributions_controller.rb @@ -0,0 +1,17 @@ +class ApiTeacherDistributionsController < ApiController + + before_filter :api_signed_in_user + respond_to :json + + def index + data = TeacherDistribution.index(current_user, params) + + @distributions = data[:query] + @next = data[:next_page] + render "api_teacher_distributions/index", :layout => nil + end + + + private + +end diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index aaff60533..ae6b4570b 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -34,7 +34,7 @@ ApiUsersController < ApiController def show @user=lookup_user - @show_teacher = params[:show_teacher] + @show_teacher = true @show_profile = params[:show_profile] @show_student = params[:show_student] diff --git a/web/app/controllers/sessions_controller.rb b/web/app/controllers/sessions_controller.rb index 45504a363..6186e18dd 100644 --- a/web/app/controllers/sessions_controller.rb +++ b/web/app/controllers/sessions_controller.rb @@ -87,6 +87,8 @@ class SessionsController < ApplicationController auth_hash = request.env['omniauth.auth'] provider = auth_hash[:provider] + provider = provider.to_s + provider.strip! if provider == 'google_login' @@ -269,7 +271,8 @@ class SessionsController < ApplicationController end - redirect_to SignupHint.most_recent_redirect(current_user, request.env['omniauth.origin'] || '/client#/home') + # request.env['omniauth.origin'] + redirect_to SignupHint.most_recent_redirect(current_user, '/client#/jamclass',{"stripe-success" => "true"}) end def has_google_auth @@ -304,6 +307,15 @@ class SessionsController < ApplicationController end def failure + + strategy = params['strategy'] + + if strategy == 'stripe_connect' + + puts env['omniauth.error.type'] + redirect_to SignupHint.most_recent_redirect(current_user, '/client#/jamclass', {"stripe-success" => "false"}) + return + end redirect_to request.query_parameters['origin'] || '/' end diff --git a/web/app/helpers/client_helper.rb b/web/app/helpers/client_helper.rb index 01a73152e..25c95128f 100644 --- a/web/app/helpers/client_helper.rb +++ b/web/app/helpers/client_helper.rb @@ -81,6 +81,6 @@ module ClientHelper gon.use_cached_session_scores = Rails.application.config.use_cached_session_scores gon.allow_both_find_algos = Rails.application.config.allow_both_find_algos - gon.stripe_publishable_key = Rails.application.config.stripe_publishable_key + gon.stripe_publishable_key = Rails.application.config.stripe[:publishable_key] end end diff --git a/web/app/helpers/music_session_helper.rb b/web/app/helpers/music_session_helper.rb index 03001dc0d..dcc9191e5 100644 --- a/web/app/helpers/music_session_helper.rb +++ b/web/app/helpers/music_session_helper.rb @@ -74,7 +74,7 @@ module MusicSessionHelper music_session.scheduled_start_date end - def pretty_scheduled_start(music_session, with_timezone) - music_session.pretty_scheduled_start(with_timezone) + def pretty_scheduled_start(music_session, with_timezone, shorter = false) + music_session.pretty_scheduled_start(with_timezone, shorter) end end diff --git a/web/app/views/api_chats/show.rabl b/web/app/views/api_chats/show.rabl index 828e09f21..3b6c57e72 100644 --- a/web/app/views/api_chats/show.rabl +++ b/web/app/views/api_chats/show.rabl @@ -1,6 +1,6 @@ object @chat -attributes :id, :message, :user_id, :session_id, :created_at, :channel +attributes :id, :message, :user_id, :session_id, :created_at, :channel, :lesson_session_id, :purpose node :user do |c| user_data = {} diff --git a/web/app/views/api_lesson_bookings/show.rabl b/web/app/views/api_lesson_bookings/show.rabl index 84fc187b5..f0e6f766c 100644 --- a/web/app/views/api_lesson_bookings/show.rabl +++ b/web/app/views/api_lesson_bookings/show.rabl @@ -1,6 +1,6 @@ object @lesson_booking -attributes :id, :status, :lesson_type, :payment_style, :recurring, :teacher_id, :description, :lesson_length, :created_at, :user_id, :active, :accepter_id, :canceler_id, :cancel_message, :booked_price +attributes :id, :status, :lesson_type, :payment_style, :recurring, :teacher_id, :description, :lesson_length, :created_at, :user_id, :active, :accepter_id, :canceler_id, :cancel_message, :booked_price, :card_presumed_ok child(:lesson_booking_slots => :slots) { attributes :id, :preferred_day, :day_of_week, :hour, :minute, :slot_type, :pretty_scheduled_start, :message, :pretty_start_time, :proposer_id, :is_student_created?, :is_teacher_created?, :timezone @@ -28,5 +28,18 @@ node :teacher do |lesson_booking| end child(:next_lesson => :next_lesson) do |next_lesson| - attributes :id, :scheduled_start, :status, :music_session_id, :pretty_scheduled_start -end \ No newline at end of file + attributes :id, :scheduled_start, :status, :music_session_id, :pretty_scheduled_start, :teacher_short_canceled +end + +if @lesson_session + node :focused_lesson do + { + id: @lesson_session.id, + scheduled_start: @lesson_session.scheduled_start, + status: @lesson_session.status, + music_session_id: @lesson_session.music_session.id, + pretty_scheduled_start: @lesson_session.pretty_scheduled_start, + teacher_short_canceled: @lesson_session.teacher_short_canceled + } + end +end diff --git a/web/app/views/api_lesson_sessions/show.rabl b/web/app/views/api_lesson_sessions/show.rabl index 2fe2c97c5..31be0b3bc 100644 --- a/web/app/views/api_lesson_sessions/show.rabl +++ b/web/app/views/api_lesson_sessions/show.rabl @@ -1,6 +1,20 @@ object @lesson_session -attributes :id, :lesson_type, :duration, :price, :teacher_complete, :student_complete, :status, :student_canceled, :teacher_canceled, :student_canceled_at, :teacher_canceled_at, :student_canceled_reason, :teacher_canceled_reason, :status + attributes :id, :lesson_booking_id, :lesson_type, :duration, :price, :teacher_complete, :student_complete, + :status, :student_canceled, :teacher_canceled, :student_canceled_at, :teacher_canceled_at, :student_canceled_reason, + :teacher_canceled_reason, :status, :success, :teacher_unread_messages, :student_unread_messages, :is_active?, :recurring, :analysed + +node do |lesson_session| + { + analysis: lesson_session.analysis_json + } +end + +child(:lesson_booking => :lesson_booking) { + + attributes :card_presumed_ok + +} child(:music_session => :music_session) { attributes :id, :music_session_id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat, @@ -18,7 +32,7 @@ child(:music_session => :music_session) { end node :pretty_scheduled_start_with_timezone do |session| - pretty_scheduled_start(session, true) + pretty_scheduled_start(session, true, true) end node :pretty_scheduled_start_short do|session| @@ -26,6 +40,11 @@ child(:music_session => :music_session) { end } -child(:teacher => :teacher) do |teacher| - partial "api_users/show", object: teacher +node(:teacher) do |lesson_session| + partial "api_users/show", object: lesson_session.teacher +end + + +node(:student) do |lesson_session| + partial "api_users/show", object: lesson_session.student end \ No newline at end of file diff --git a/web/app/views/api_lesson_sessions/update_unread_messages.rabl b/web/app/views/api_lesson_sessions/update_unread_messages.rabl new file mode 100644 index 000000000..733df9746 --- /dev/null +++ b/web/app/views/api_lesson_sessions/update_unread_messages.rabl @@ -0,0 +1,3 @@ +object @lesson_session + +extends "api_lesson_sessions/show" \ No newline at end of file diff --git a/web/app/views/api_music_sessions/show_history.rabl b/web/app/views/api_music_sessions/show_history.rabl index 700b00cf8..19d75ee64 100644 --- a/web/app/views/api_music_sessions/show_history.rabl +++ b/web/app/views/api_music_sessions/show_history.rabl @@ -51,6 +51,7 @@ else pretty_scheduled_start(session, false) end + child(:creator => :creator) { attributes :id, :name, :photo_url } diff --git a/web/app/views/api_payment_histories/show.rabl b/web/app/views/api_payment_histories/show.rabl index f86cf10b4..bf330b0bb 100644 --- a/web/app/views/api_payment_histories/show.rabl +++ b/web/app/views/api_payment_histories/show.rabl @@ -1,7 +1,7 @@ object @payment_history child :sale do - attributes :id, :recurly_invoice_id, :recurly_subtotal_in_cents, :recurly_tax_in_cents, :recurly_total_in_cents, :recurly_currency, :sale_type, :recurly_invoice_number, :state, :created_at + attributes :id, :recurly_invoice_id, :recurly_subtotal_in_cents, :recurly_tax_in_cents, :recurly_total_in_cents, :recurly_currency, :sale_type, :recurly_invoice_number, :state, :created_at, :source child(:sale_line_items => :line_items) { attributes :id, :product_info diff --git a/web/app/views/api_stripe/store.rabl b/web/app/views/api_stripe/store.rabl index 90555669c..953850168 100644 --- a/web/app/views/api_stripe/store.rabl +++ b/web/app/views/api_stripe/store.rabl @@ -1,23 +1,18 @@ object @lesson -node :lesson do |lesson| - attribute :id +if @lesson + node :lesson do |lesson| + {id: @lesson.id } + end end + if @test_drive node :test_drive do |lesson| attributes :teacher_id end end -if @intent - node :intent_book_test_drive do |lesson| - { - teacher_id: @intent.teacher.user.id - } - end -end - if @normal node :normal do |lesson| diff --git a/web/app/views/api_teacher_distributions/index.rabl b/web/app/views/api_teacher_distributions/index.rabl new file mode 100644 index 000000000..ab6892198 --- /dev/null +++ b/web/app/views/api_teacher_distributions/index.rabl @@ -0,0 +1,11 @@ +node :next do |page| + @next +end + +node :entries do |page| + partial "api_teacher_distributions/show", object: @teacher_distributions +end + +node :total_entries do |page| + @teacher_distributions.total_entries +end diff --git a/web/app/views/api_teacher_distributions/show.rabl b/web/app/views/api_teacher_distributions/show.rabl new file mode 100644 index 000000000..2f9ac2d82 --- /dev/null +++ b/web/app/views/api_teacher_distributions/show.rabl @@ -0,0 +1,4 @@ +object @teacher_distribution + +attributes :id, :description, :ready, :distributed, :created_at + diff --git a/web/app/views/api_teachers/detail.rabl b/web/app/views/api_teachers/detail.rabl index 5941f00c1..86c843c39 100644 --- a/web/app/views/api_teachers/detail.rabl +++ b/web/app/views/api_teachers/detail.rabl @@ -34,7 +34,9 @@ attributes :id, :years_teaching, :teaches_test_drive, :test_drives_per_week, - :errors + :errors, + :profile_pct + child :review_summary => :review_summary do attributes :avg_rating, :wilson_score, :review_count @@ -49,6 +51,9 @@ child :recent_reviews => :recent_reviews do } end +node :profile_pct_summary do |teacher| + JSON.parse(teacher.profile_pct_summary) +end node :instruments do |teacher| diff --git a/web/app/views/api_users/notification_index.rabl b/web/app/views/api_users/notification_index.rabl index f9d8cdef1..b86640aff 100644 --- a/web/app/views/api_users/notification_index.rabl +++ b/web/app/views/api_users/notification_index.rabl @@ -1,6 +1,6 @@ collection @notifications -attributes :description, :source_user_id, :target_user_id, :session_id, :recording_id, :invitation_id, :join_request_id, :friend_request_id, :band_id, :band_invitation_id, :formatted_msg, :message, :created_at, :lesson_session_id +attributes :description, :source_user_id, :target_user_id, :session_id, :recording_id, :invitation_id, :join_request_id, :friend_request_id, :band_id, :band_invitation_id, :formatted_msg, :message, :created_at, :lesson_session_id, :purpose node :source_user do |n| source_user_data = {} diff --git a/web/app/views/api_users/show.rabl b/web/app/views/api_users/show.rabl index eb9c67be2..11cf23d4c 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 +: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 node :location do |user| if user.musician? @@ -19,8 +19,6 @@ node :teacher do |user| end - - if @show_profile node :profile do partial("api_users/profile_show", :object => @user) diff --git a/web/app/views/clients/_account_payment_history.html.slim b/web/app/views/clients/_account_payment_history.html.slim index e430bc90d..661425132 100644 --- a/web/app/views/clients/_account_payment_history.html.slim +++ b/web/app/views/clients/_account_payment_history.html.slim @@ -6,26 +6,7 @@ h1 my account =render "screen_navigation" .content-body - .content-body-scroller - - .account-left - h2 payment history: - table.payment-table - thead - tr - th DATE - th METHOD - th DESCRIPTION - th STATUS - th AMOUNT - tbody - a.btn-next-pager href="/api/sales?page=1" Next - .end-of-payments-list.end-of-list="No more payment history" - - - .input-aligner - a.back href="" class="button-grey" BACK - br clear="all" + = react_component 'AccountPaymentHistoryScreen', {} diff --git a/web/app/views/clients/_lessonSessionActions.html.slim b/web/app/views/clients/_lessonSessionActions.html.slim new file mode 100644 index 000000000..dc75c62a6 --- /dev/null +++ b/web/app/views/clients/_lessonSessionActions.html.slim @@ -0,0 +1,59 @@ +script type='text/template' id='template-lesson-session-actions' + + = '{% if (data.cardNotOk) { %}' + + li data-lesson-option="enter-payment" + a href='#' Enter Payment + + = '{% } else if (data.isRequested) { %}' + ul + li data-lesson-option="status" + a href='#' View Status + + li data-lesson-option="messages" + a href='#' View Messages + + li data-lesson-option="messages" + a href='#' Attach Message + + li data-lesson-option="cancel" + a href='#' Decline Request + + = '{% } else if (data.isScheduled) { %}' + ul + li data-lesson-option="status" + a href='#' View Status + + li data-lesson-option="messages" + a href='#' View Messages + + li data-lesson-option="messages" + a href='#' Attach Message + + li data-lesson-option="join" + a href='#' Join Lesson Session + + li data-lesson-option="reschedule" + a href='#' Reschedule Lesson + + li data-lesson-option="cancel" + a href='#' Cancel Lesson + + = '{% if (data.isAdmin) { %}' + li data-lesson-option="start-5-min" + a href='#' Set Start In 5 Min + li data-lesson-option="start-35-ago" + a href='#' Set Start 35 Min Ago + = '{% } %}' + + = '{% } else { %}' + ul + li data-lesson-option="status" + a href='#' View Status + + li data-lesson-option="messages" + a href='#' View Messages + + li data-lesson-option="messages" + a href='#' Attach Message + = '{% } %}' \ 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 eb19d521d..be0999f7c 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -21,6 +21,7 @@ <%= render "iconInstrumentSelect" %> <%= render "muteSelect" %> <%= render "manageVsts" %> +<%= render "lessonSessionActions" %> <%= render "vstEffects" %> <%= render "metronome_playback_mode" %> <%= render "clients/wizard/buttons" %> @@ -267,8 +268,8 @@ var accountVideoProfile = new JK.AccountVideoProfile(JK.app); accountVideoProfile.initialize(); - var accountPaymentHistoryScreen = new JK.AccountPaymentHistoryScreen(JK.app); - accountPaymentHistoryScreen.initialize(); + //var accountPaymentHistoryScreen = new JK.AccountPaymentHistoryScreen(JK.app); + //accountPaymentHistoryScreen.initialize(); var searchResultScreen = new JK.SearchResultScreen(JK.app); searchResultScreen.initialize(); diff --git a/web/app/views/clients/jamclass/_jamclass_student.html.slim b/web/app/views/clients/jamclass/_jamclass_student.html.slim index 3607f633c..fef7166d4 100644 --- a/web/app/views/clients/jamclass/_jamclass_student.html.slim +++ b/web/app/views/clients/jamclass/_jamclass_student.html.slim @@ -6,5 +6,5 @@ | jamclass = render "screen_navigation" .content-body - = react_component 'JamClassStudentScreen', {} + = react_component 'JamClassScreen', {} diff --git a/web/app/views/clients/jamclass/_lesson_payment.html.slim b/web/app/views/clients/jamclass/_lesson_payment.html.slim index 6cfa5917d..39feec078 100644 --- a/web/app/views/clients/jamclass/_lesson_payment.html.slim +++ b/web/app/views/clients/jamclass/_lesson_payment.html.slim @@ -1,4 +1,4 @@ -#lesson-payment.screen.secondary layout="screen" layout-id="jamclass/lesson-payment" +#lesson-payment.screen.secondary layout="screen" layout-id="jamclass/lesson-payment" layout-arg="id" .content-head .content-icon = image_tag "content/icon_account.png", :size => "27x20" diff --git a/web/app/views/dialogs/_cancelLessonDialog.html.slim b/web/app/views/dialogs/_cancelLessonDialog.html.slim new file mode 100644 index 000000000..996fbc65f --- /dev/null +++ b/web/app/views/dialogs/_cancelLessonDialog.html.slim @@ -0,0 +1,2 @@ +.dialog.dialog-overlay-sm.top-parent layout='dialog' layout-id='cancel-lesson-dialog' id='cancel-lesson-dialog' + = react_component 'CancelLessonDialog', {} diff --git a/web/app/views/dialogs/_chatDialog.html.slim b/web/app/views/dialogs/_chatDialog.html.slim new file mode 100644 index 000000000..3d0255af5 --- /dev/null +++ b/web/app/views/dialogs/_chatDialog.html.slim @@ -0,0 +1,2 @@ +.dialog.dialog-overlay-sm.top-parent layout='dialog' layout-id='chat-dialog' id='chat-dialog' + = react_component 'ChatDialog', {} diff --git a/web/app/views/dialogs/_dialogs.html.haml b/web/app/views/dialogs/_dialogs.html.haml index 196245aa2..ae4d9e5fb 100644 --- a/web/app/views/dialogs/_dialogs.html.haml +++ b/web/app/views/dialogs/_dialogs.html.haml @@ -49,4 +49,7 @@ = render 'dialogs/manageVstsDialog' = render 'dialogs/tryTestDriveDialog' = render 'dialogs/uploadAvatarDialog' -= render 'dialogs/inviteSchoolUserDialog' \ No newline at end of file += render 'dialogs/inviteSchoolUserDialog' += render 'dialogs/chatDialog' += render 'dialogs/cancelLessonDialog' += render 'dialogs/rescheduleLessonDialog' diff --git a/web/app/views/dialogs/_rescheduleLessonDialog.html.slim b/web/app/views/dialogs/_rescheduleLessonDialog.html.slim new file mode 100644 index 000000000..fa142f050 --- /dev/null +++ b/web/app/views/dialogs/_rescheduleLessonDialog.html.slim @@ -0,0 +1,2 @@ +.dialog.dialog-overlay-sm.top-parent layout='dialog' layout-id='reschedule-lesson-dialog' id='reschedule-lesson-dialog' + = react_component 'RescheduleLessonDialog', {} diff --git a/web/config/application.rb b/web/config/application.rb index 8a22c19b7..3638d9aa3 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -164,8 +164,6 @@ if defined?(Bundler) # Use Public Keys to identify your site when using Recurly.js. See https://docs.recurly.com/js/#include to learn more. config.recurly_public_api_key = 'sjc-SZlO11shkeA1WMGuISLGg5' - config.stripe_secret_key = 'sk_test_cPVRbtr9xbMiqffV8jwibwLA' - config.stripe_publishable_key = 'pk_test_9vO8ZnxBpb9Udb0paruV3qLv' config.stripe_charge_token = '#XXX#' if Rails.env == 'production' @@ -429,6 +427,7 @@ if defined?(Bundler) :secret_key => 'sk_test_cPVRbtr9xbMiqffV8jwibwLA', :client_id => 'ca_8CgkjoHvfRMVqoQkcKdPt5Riy3dSPIlg' } + config.test_drive_wait_period_year = 1 config.minimum_lesson_booking_hrs = 24 config.wait_time_window_pct = 0.75 diff --git a/web/config/routes.rb b/web/config/routes.rb index 1b82d962b..7d81ca51f 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -687,13 +687,20 @@ SampleApp::Application.routes.draw do match '/links/recordings' => 'api_links#recording_index' match '/lesson_sessions' => 'api_lesson_sessions#index', :via => :get + match '/lesson_bookings/unprocessed' => 'api_lesson_bookings#unprocessed', :via => :get + match '/lesson_bookings/unprocessed_or_intent' => 'api_lesson_bookings#unprocessed_or_intent', :via => :get + + match '/lesson_sessions/:id' => 'api_lesson_sessions#show', :via => :get + match '/lesson_sessions/:id/update_unread_messages' => 'api_lesson_sessions#update_unread_messages', :via => :post + match '/lesson_sessions/:id/start_time' => 'api_lesson_sessions#start_time', :via => :post + match '/lesson_sessions/:id/reschedule_check' => 'api_lesson_sessions#reschedule_check', :via => :post + match '/lesson_sessions/:id/cancel_check' => 'api_lesson_sessions#cancel_check', :via => :post match '/lesson_bookings' => 'api_lesson_bookings#create', :via => :post match '/lesson_bookings/:id/accept' => 'api_lesson_bookings#accept', :via => :post match '/lesson_bookings/:id/counter' => 'api_lesson_bookings#counter', :via => :post match '/lesson_bookings/:id/cancel' => 'api_lesson_bookings#cancel', :via => :post match '/lesson_bookings/:id' => 'api_lesson_bookings#show', :via => :get - match '/lesson_bookings/unprocessed' => 'api_lesson_bookings#unprocessed', :via => :get - match '/lesson_bookings/unprocessed_or_intent' => 'api_lesson_bookings#unprocessed_or_intent', :via => :get + match '/schools/:id' => 'api_schools#show', :via => :get match '/schools/:id' => 'api_schools#update', :via => :post @@ -707,6 +714,8 @@ SampleApp::Application.routes.draw do match '/schools/:id/students/:user_id' => 'api_schools#remove_student', :via => :delete match '/schools/:id/teachers/:teacher_id' => 'api_schools#remove_teacher', :via => :delete + match '/teacher_distributions' => 'api_teacher_distributions#index', :via => :get + match '/stripe' => 'api_stripe#store', :via => :post match 'desktopclient/canpair' => 'api_jamblasters#can_pair', :via => :get match 'jamblasters/pairing/isallowed' => 'api_jamblasters#is_allowed', :via => :get diff --git a/web/config/scheduler.yml b/web/config/scheduler.yml index 339dfd4f6..c98fab231 100644 --- a/web/config/scheduler.yml +++ b/web/config/scheduler.yml @@ -45,6 +45,16 @@ DailyJob: class: "JamRuby::DailyJob" description: "Aggregate task to perform general daily things" +HourlyJob: + cron: "0 * * * *" + class: "JamRuby::HourlyJob" + description: "Aggregate task to perform general hourly things" + +MinutelyJob: + cron: "* * * * *" + class: "JamRuby::MinutelyJob" + description: "Aggregrate task to perform general minutely things" + ScheduledMusicSessionCleaner: cron: "0 3 * * *" class: "JamRuby::ScheduledMusicSessionCleaner" diff --git a/web/spec/factories.rb b/web/spec/factories.rb index 24e9cf5cd..cc6d5d9a4 100644 --- a/web/spec/factories.rb +++ b/web/spec/factories.rb @@ -108,6 +108,18 @@ FactoryGirl.define do connection = FactoryGirl.create(:connection, :user => user, :music_session => music_session) end end + + factory :teacher_user do + after(:create) do |user, evaluator| + teacher = FactoryGirl.create(:teacher, user: user, price_per_lesson_60_cents: 3000, price_per_month_60_cents: 3000, lesson_duration_60: true, prices_per_lesson: true, prices_per_month: true) + end + end + end + + factory :teacher, :class => JamRuby::Teacher do + association :user, factory: :user + price_per_lesson_60_cents 3000 + price_per_month_60_cents 3000 end factory :musician_instrument, :class=> JamRuby::MusicianInstrument do diff --git a/web/spec/features/book_single_lesson_spec.rb b/web/spec/features/book_single_lesson_spec.rb new file mode 100644 index 000000000..831e4231d --- /dev/null +++ b/web/spec/features/book_single_lesson_spec.rb @@ -0,0 +1,106 @@ +require 'spec_helper' + +describe "Single Lesson", :js => true, :type => :feature, :capybara_feature => true do + + subject { page } + + let(:user) { FactoryGirl.create(:user, traditional_band: true,paid_sessions: true, paid_sessions_hourly_rate: 1, paid_sessions_daily_rate:1 ) } + let(:teacher_user) {FactoryGirl.create(:teacher_user, ready_for_session_at: Time.now)} + + before(:each) do + LessonBooking.destroy_all + Recording.delete_all + Diagnostic.delete_all + User.delete_all + + UserMailer.deliveries.clear + emulate_client + sign_in_poltergeist user + + + teacher_user.teacher.ready_for_session_at = Time.now + teacher_user.teacher.save! + + teacher_user.teacher.price_per_lesson_60_cents.should eql 3000 + + Teacher.index(user, {})[:query].count.should eql 1 + + + end + + def finish_lesson_successfully + + end + + describe "register via showing interesting in teacher 1st" do + after(:each) do + Timecop.return + end + it "succeeds" do + + visit "/client#/teachers/search" + + Timecop.travel(Date.new(2016, 04, 01)) + + find('.teacher-search-result[data-teacher-id="' + teacher_user.id + '"] .try-normal').trigger(:click) + + # book the lesson + fill_in "slot-1-date", with: "Sun Apr 17 2016" + #find('.slot.slot-1 input.hasDatepicker').trigger(:click) + # click 4-6 + find('td a', text: '17').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 'user-description', with: 'abc def dog neck' + + select('60 Minute Lesson for $30.00', :from => "booking-options-for-teacher") + sleep 3 + + find('a.book-lesson-btn', text: 'BOOK LESSON').trigger(:click) + + #find('h2', text: 'your lesson has been requested') + + find('h2', text: 'enter payment info for lesson') + + user.student_lesson_bookings.count.should eql 1 + lesson_booking = user.student_lesson_bookings.first + lesson_booking.is_requested?.should be_true + lesson_booking.card_presumed_ok.should be_false + + + + fill_in 'card-number', with: '4111111111111111' + fill_in 'expiration', with: '11/2016' + fill_in 'cvv', with: '111' + fill_in 'zip', with: '78759' + + find('.purchase-btn').trigger(:click) + + # 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) + + lesson_session = LessonSession.order('created_at desc').first + + # validate that we made a lesson purchase + lesson_package_purchase = LessonPackagePurchase.where(user_id: user.id).first + lesson_package_purchase.should be_nil + user.reload + user.remaining_test_drives.should eql 0 + #lesson_package_purchase.amount_charged.should eql 49.99 + user.sales.count.should eql 0 + + # the spec says take them back to search; there is some wasted effort here by the student; they have to click the teacher 2x. Ok? + find('h2', text: 'my lessons') + find('tr[data-lesson-session-id="' + lesson_session.id + '"] .lesson-session-actions-btn').trigger(:click) + + + finish_lesson_successfully + end + end +end diff --git a/web/spec/features/book_test_drive_spec.rb b/web/spec/features/book_test_drive_spec.rb new file mode 100644 index 000000000..c6c60040f --- /dev/null +++ b/web/spec/features/book_test_drive_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' + +describe "Test Drive", :js => true, :type => :feature, :capybara_feature => true do + + subject { page } + + let(:user) { FactoryGirl.create(:user, traditional_band: true,paid_sessions: true, paid_sessions_hourly_rate: 1, paid_sessions_daily_rate:1 ) } + let(:teacher_user) {FactoryGirl.create(:teacher_user, ready_for_session_at: Time.now)} + + before(:each) do + LessonBooking.destroy_all + Recording.delete_all + Diagnostic.delete_all + User.delete_all + + UserMailer.deliveries.clear + emulate_client + sign_in_poltergeist user + + + teacher_user.teacher.ready_for_session_at = Time.now + teacher_user.teacher.save! + + Teacher.index(user, {})[:query].count.should eql 1 + + + end + + def finish_lesson_successfully + + end + + describe "register via showing interesting in teacher 1st" do + after(:each) do + Timecop.return + end + it "succeeds" do + + 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) + + # TryTestDriveDialog shows + find('.purchase-testdrive-now').trigger(:click) + + fill_in 'card-number', with: '4111111111111111' + fill_in 'expiration', with: '11/2016' + fill_in 'cvv', with: '111' + fill_in 'zip', with: '78759' + + find('.purchase-btn').trigger(:click) + + # we tell user they have test drive purchased, and take them to the teacher screen + find('#banner h1', text: 'Test Drive Purchased') + # 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.lesson_payment_charge.should_not be_nil + user.reload + user.remaining_test_drives.should eql 4 + #lesson_package_purchase.amount_charged.should eql 49.99 + user.sales.count.should eql 1 + sale = user.sales.first + sale.recurly_total_in_cents.should eql 5411 + + # the spec says take them back to search; there is some wasted effort here by the student; they have to click the teacher 2x. Ok? + find('.teacher-search-result[data-teacher-id="' + teacher_user.id + '"] .try-test-drive').trigger(:click) + + # book the lesson + fill_in "slot-1-date", with: "Sun Apr 17 2016" + #find('.slot.slot-1 input.hasDatepicker').trigger(:click) + # click 4-6 + find('td a', text: '17').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 'user-description', with: 'abc def dog neck' + + sleep 3 + + + find('a.book-lesson-btn', text: 'BOOK TESTDRIVE LESSON').trigger(:click) + + find('h2', text: 'your lesson has been requested') + + user.student_lesson_bookings.count.should eql 1 + lesson_booking = user.student_lesson_bookings.first + lesson_booking.is_requested?.should be_true + + + finish_lesson_successfully + end + end +end diff --git a/web/spec/support/app_config.rb b/web/spec/support/app_config.rb index aa9283cd0..fa7b51d58 100644 --- a/web/spec/support/app_config.rb +++ b/web/spec/support/app_config.rb @@ -134,6 +134,14 @@ def web_config def guard_against_browser_fraud true end + + def stripe + { + :publishable_key => 'pk_test_HLTvioRAxN3hr5fNfrztZeoX', + :secret_key => 'sk_test_OkjoIF7FmdjunyNsdVqJD02D', + :source_customer => 'cus_88Vp44SLnBWMXq' # seth@jamkazam.com in JamKazam-test account + } + end end klass.new end From a4a869535e877640b44ebb544772ab6cefcc1dbc Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 21 Apr 2016 09:51:28 -0500 Subject: [PATCH 02/36] * force pg_migrate version- --- web/Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/web/Gemfile b/web/Gemfile index b78176c6f..6e7c0966b 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -19,6 +19,7 @@ else end #gem 'license_finder' +gem 'pg_migrate', '0.1.14' gem 'kickbox' gem 'oj', '2.10.2' gem 'builder' From 71df46a6ca91c12d661994287c894824fe65a1e6 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 21 Apr 2016 10:51:04 -0500 Subject: [PATCH 03/36] Fix bad test drive count on booking page and add nav --- web/app/assets/javascripts/jam_rest.js | 2 +- .../BookLessonFree.js.jsx.coffee | 4 +++- .../stores/BroadcastStore.js.coffee | 6 ++++-- .../stylesheets/client/jamkazam.css.scss | 4 ++++ .../react-components/BookLessonFree.css.scss | 8 ++++++++ .../react-components/LessonBooking.css.scss | 8 -------- .../LessonBookingScreen.css.scss | 5 ----- web/app/controllers/api_users_controller.rb | 2 +- web/spec/features/book_test_drive_spec.rb | 19 +++++++++++++++++++ 9 files changed, 40 insertions(+), 18 deletions(-) diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 7a4cbd546..b3c03627c 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -587,7 +587,7 @@ processData: false }); } - if(detail) { + if(detail && context.JK.currentUserId == id) { detail.done(function(user) { window.UserActions.loaded(user) }) diff --git a/web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee b/web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee index 97793c160..e0ed48dd6 100644 --- a/web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/BookLessonFree.js.jsx.coffee @@ -17,6 +17,7 @@ UserStore = context.UserStore {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide}) onUserChanged: (userState) -> + console.log("userState", userState) @setState({user: userState?.user}) checkboxChanged: (e) -> @@ -435,7 +436,7 @@ UserStore = context.UserStore if @state.user?.remaining_test_drives == 1 testDriveLessons = "1 TestDrive lesson credit" else - testDriveLessons = "#{this.state.user?.remaining_test_drives} TestDrive lesson credits" + testDriveLessons = "#{this.state.user.remaining_test_drives} TestDrive lesson credits" actions = `
CANCEL @@ -515,6 +516,7 @@ UserStore = context.UserStore
` `
+
` }) \ 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 0a2273705..a21daafc3 100644 --- a/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee @@ -258,7 +258,7 @@ ProfileActions = @ProfileActions
JamKazam Home :  JamClass Home :  - Teachers Search :  + Teachers Search :  Search Results / {searchDesc} diff --git a/web/app/assets/stylesheets/client/react-components/BookLessonFree.css.scss b/web/app/assets/stylesheets/client/react-components/BookLesson.css.scss similarity index 100% rename from web/app/assets/stylesheets/client/react-components/BookLessonFree.css.scss rename to web/app/assets/stylesheets/client/react-components/BookLesson.css.scss diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index be0999f7c..297594f77 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -46,7 +46,7 @@ <%= render "clients/teachers/profile/profile" %> <%= render "clients/teachers/search/search_options" %> <%= render "clients/teachers/search/search_results" %> -<%= render "clients/jamclass/book_lesson_free" %> +<%= render "clients/jamclass/book_lesson" %> <%= render "clients/jamclass/lesson_payment" %> <%= render "clients/jamclass/lesson_session" %> <%= render "clients/jamclass/lesson_booking" %> diff --git a/web/app/views/clients/jamclass/_book_lesson_free.html.slim b/web/app/views/clients/jamclass/_book_lesson.html.slim similarity index 100% rename from web/app/views/clients/jamclass/_book_lesson_free.html.slim rename to web/app/views/clients/jamclass/_book_lesson.html.slim diff --git a/web/spec/features/book_test_drive_spec.rb b/web/spec/features/book_test_drive_spec.rb index 146d00ca2..ecbf4e627 100644 --- a/web/spec/features/book_test_drive_spec.rb +++ b/web/spec/features/book_test_drive_spec.rb @@ -104,14 +104,15 @@ describe "Test Drive", :js => true, :type => :feature, :capybara_feature => true # let's make sure we can ask for another test drive too! - teacher_user2.teacher.ready_for_session_at = Time.now teacher_user2.teacher.save! + visit "/client#/teachers/search" + find('a.teacher-search-options').trigger(:click) + find('a.search-btn').trigger(:click) find('.teacher-search-result[data-teacher-id="' + teacher_user2.id + '"] .try-test-drive').trigger(:click) - visit "/client#/teachers/search" find('h2', text: 'book testdrive lesson') find('.booking-info', text: '3 TestDrive lesson credits') From a14b58f2fe1ac3e6f05886cca87f5010b41eff7f Mon Sep 17 00:00:00 2001 From: Seth Call Date: Fri, 22 Apr 2016 14:59:48 -0500 Subject: [PATCH 05/36] Home screen and session create screen re-worked --- ruby/lib/jam_ruby/app/mailers/user_mailer.rb | 25 ++ .../student_lesson_normal_no_bill.html.erb | 2 +- .../student_lesson_normal_no_bill.text.erb | 2 +- .../student_test_drive_no_bill.html.erb | 23 ++ .../student_test_drive_no_bill.text.erb | 5 + ruby/lib/jam_ruby/models/music_session.rb | 29 ++- ruby/lib/jam_ruby/models/user.rb | 2 +- .../jam_ruby/models/lesson_booking_spec.rb | 6 +- .../jam_ruby/models/music_session_spec.rb | 25 ++ web/app/assets/javascripts/homeScreen.js | 7 + web/app/assets/javascripts/layout.js | 4 +- .../react-components/BookLesson.js.jsx.coffee | 1 - .../javascripts/scheduled_session.js.erb | 246 +++++++++++++++--- .../stylesheets/client/createSession.css.scss | 97 ++++++- web/app/views/clients/_home.html.slim | 67 ++--- .../views/clients/_scheduledSession.html.erb | 85 ++++-- 16 files changed, 500 insertions(+), 126 deletions(-) create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.text.erb diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb index d6dd849ca..25e739518 100644 --- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb @@ -927,6 +927,31 @@ module JamRuby end end + def student_test_drive_no_bill(lesson_session) + @student = lesson_session.student + @teacher = lesson_session.teacher + @session_name = lesson_session.music_session.name + @session_description = lesson_session.music_session.description + @session_date = lesson_session.slot.pretty_scheduled_start(true) + @session_url = lesson_session.web_url + @lesson_session = lesson_session + + email = @student.email + subject = "Your TestDrive with #{@teacher.name} will not be billed" + unique_args = {:type => "student_test_drive_no_bill"} + + sendgrid_category "Notification" + sendgrid_unique_args :type => unique_args[:type] + + sendgrid_recipients([email]) + sendgrid_substitute('@USERID', [@student.id]) + + mail(:to => email, :subject => subject) do |format| + format.text + format.html { render :layout => "from_user_mailer" } + end + end + # successfully completed, but no more test drives left def student_test_drive_lesson_done(lesson_session) diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb index f7836f6ce..aa263319f 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb @@ -6,7 +6,7 @@ Hello <%= @student.name %>,

-

You will not be billed for today's session with <%= @teacher.name %> +

You will not be billed for today's session with <%= @teacher.name %>.

Click the button below to see more information about this session. diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.text.erb index 0c6d60a65..31ef00ca0 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.text.erb @@ -1,5 +1,5 @@ Hello <%= @student.name %>, -You will not be billed for today's session with <%= @teacher.name %> +You will not be billed for today's session with <%= @teacher.name %>. To see this lesson, click here: <%= @lesson_session.web_url %> diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.html.erb new file mode 100644 index 000000000..deee9c563 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.html.erb @@ -0,0 +1,23 @@ +<% provide(:title, "Your TestDrive with #{@teacher.name} will not be billed") %> +<% provide(:photo_url, @teacher.resolved_photo_url) %> + +<% content_for :note do %> +

+ Hello <%= @student.name %>, +

+ +

You have not used a credit for today's TestDrive with <%= @teacher.name %>. +
+
+ Click the button below to see more information about this session. +

+

+ VIEW + LESSON DETAILS +

+

+ Best Regards,
Team JamKazam +

+<% end %> + + diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.text.erb new file mode 100644 index 000000000..f3d3b2825 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.text.erb @@ -0,0 +1,5 @@ +Hello <%= @student.name %>, + +You have not used a credit for today's TestDrive with <%= @teacher.name %>. + +To see this lesson, click here: <%= @lesson_session.web_url %> diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index 14947dd74..d03d8ac67 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -22,6 +22,7 @@ module JamRuby CREATE_TYPE_IMMEDIATE = 'immediately' CREATE_TYPE_QUICK_START = 'quick-start' CREATE_TYPE_LESSON = 'lesson' + CREATE_TYPE_QUICK_PUBLIC = 'quick-public' attr_accessor :legal_terms, :language_description, :access_description, :scheduling_info_changed @@ -323,11 +324,29 @@ module JamRuby # let session be restarted for up to 2 hours after finishing session_finished = "(music_sessions.session_removed_at > NOW() - '2 hour'::INTERVAL)" - query = MusicSession.where("music_sessions.canceled = FALSE") + query = MusicSession.joins( + %Q{ + LEFT OUTER JOIN + invitations + ON + music_sessions.id = invitations.music_session_id AND invitations.receiver_id = '#{user.id}' + } + ) + query = query.where("music_sessions.canceled = FALSE") query = query.where('music_sessions.fan_access = TRUE or music_sessions.musician_access = TRUE') if only_public - query = query.where("music_sessions.user_id = '#{user.id}'") + #query = query.where("music_sessions.user_id = '#{user.id}' OR invitations.id IS NOT NULL") + query = query.where("music_sessions.id in ( + select distinct(rs.music_session_id) + from rsvp_slots rs + where rs.id in ( + select rrrs.rsvp_slot_id + from rsvp_requests rr + inner join rsvp_requests_rsvp_slots rrrs on rr.id = rrrs.rsvp_request_id + where rr.user_id = '#{user.id}'AND rrrs.chosen = true + ) + ) OR invitations.id IS NOT NULL OR music_sessions.user_id = '#{user.id}'") query = query.where("music_sessions.scheduled_start IS NULL OR #{session_not_started} OR #{session_finished} OR #{session_started_not_finished}") - query = query.where("music_sessions.create_type IS NULL OR music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}'") + query = query.where("music_sessions.create_type IS NULL OR (music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}' AND music_sessions.create_type != '#{CREATE_TYPE_QUICK_PUBLIC}')") query = query.order("music_sessions.scheduled_start ASC") query @@ -339,7 +358,7 @@ module JamRuby filter_approved = only_approved ? 'AND rrrs.chosen = true' : '' MusicSession.where(%Q{music_sessions.canceled = FALSE AND - (music_sessions.create_type is NULL OR music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}') AND + (music_sessions.create_type is NULL OR (music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}' AND music_sessions.create_type != '#{CREATE_TYPE_QUICK_PUBLIC}')) AND (music_sessions.scheduled_start is NULL OR music_sessions.scheduled_start > NOW() - '4 hour'::INTERVAL) AND music_sessions.id in ( select distinct(rs.music_session_id) @@ -760,7 +779,7 @@ module JamRuby query = query.offset(offset) query = query.limit(limit) - query = query.where("music_sessions.create_type IS NULL OR (music_sessions.create_type != ? AND music_sessions.create_type != ?)", MusicSession::CREATE_TYPE_QUICK_START, MusicSession::CREATE_TYPE_IMMEDIATE) + query = query.where("music_sessions.create_type IS NULL OR (music_sessions.create_type != ? AND music_sessions.create_type != ? AND music_sessions.create_type != ?)", MusicSession::CREATE_TYPE_QUICK_START, MusicSession::CREATE_TYPE_IMMEDIATE, MusicSession::CREATE_TYPE_QUICK_PUBLIC) query = query.where("music_sessions.genre_id = ?", genre) unless genre.blank? query = query.where('music_sessions.language = ?', lang) unless lang.blank? query = query.where("(description_tsv @@ to_tsquery('jamenglish', ?))", ActiveRecord::Base.connection.quote(keyword) + ':*') unless keyword.blank? diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index de83de27b..448d62cb9 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -2051,7 +2051,7 @@ module JamRuby self.remaining_test_drives = self.remaining_test_drives + 1 self.save(validate: false) - UserMailer.test_drive_no_bill(self, lesson_session).deliver + UserMailer.student_test_drive_no_bill(lesson_session).deliver end def used_test_drives diff --git a/ruby/spec/jam_ruby/models/lesson_booking_spec.rb b/ruby/spec/jam_ruby/models/lesson_booking_spec.rb index 4a17aea5b..0c2bab1dd 100644 --- a/ruby/spec/jam_ruby/models/lesson_booking_spec.rb +++ b/ruby/spec/jam_ruby/models/lesson_booking_spec.rb @@ -112,7 +112,7 @@ describe LessonBooking do purchase.last_billing_attempt_at.should eql time purchase.sent_billing_notices.should eql false - user.card_approved(create_stripe_token, '78759') + user.card_approved(create_stripe_token, '78759', booking.id) user.save! day = day + 1 @@ -137,7 +137,7 @@ describe LessonBooking do end it "advances to next month" do - user.card_approved(create_stripe_token, '78759') + user.card_approved(create_stripe_token, '78759', booking.id) user.save! day = Date.new(2016, 1, 20) @@ -258,7 +258,7 @@ describe LessonBooking do purchase.billed.should be false # now that it's suspended, let's unsuspend it - user.card_approved(create_stripe_token, '78759') + user.card_approved(create_stripe_token, '78759', booking.id) user.save! day = day + 1 diff --git a/ruby/spec/jam_ruby/models/music_session_spec.rb b/ruby/spec/jam_ruby/models/music_session_spec.rb index e152fed55..4013eb804 100644 --- a/ruby/spec/jam_ruby/models/music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/music_session_spec.rb @@ -362,6 +362,31 @@ describe MusicSession do end describe "scheduled" do + + it "includes any RSVP'ed" do + rsvp_request = FactoryGirl.create(:rsvp_request_for_multiple_slots, user: some_user, music_session: music_session1, number: 2, chosen:true) + + approved_rsvps = music_session1.approved_rsvps + approved_rsvps.length.should == 2 + + + sessions = MusicSession.scheduled(approved_rsvps[0]) + sessions.length.should == 1 + + sessions = MusicSession.scheduled(approved_rsvps[1]) + sessions.length.should == 1 + end + + it "includes invited" do + invitee = FactoryGirl.create(:user, last_jam_audio_latency: 30, last_jam_locidispid: 3) + FactoryGirl.create(:friendship, user: creator, friend: invitee) + FactoryGirl.create(:friendship, user: invitee, friend: creator) + music_session = FactoryGirl.create(:music_session, creator: creator) + FactoryGirl.create(:invitation, receiver:invitee, sender:creator, music_session: music_session) + + sessions = MusicSession.scheduled(invitee) + sessions.length.should == 1 + end it "excludes based on time-range" do session = FactoryGirl.create(:music_session, scheduled_start: Time.now) diff --git a/web/app/assets/javascripts/homeScreen.js b/web/app/assets/javascripts/homeScreen.js index a96075b47..ef94e580f 100644 --- a/web/app/assets/javascripts/homeScreen.js +++ b/web/app/assets/javascripts/homeScreen.js @@ -89,6 +89,13 @@ if(gon.allow_force_native_client) { $('body').on('keyup', switchClientMode); } + + $('.homecard.jamclass').on('click', function() { + + context.JK.Banner.showNotice("Coming Soon!", "JamClass is just around the corner.") + + return false; + }) } this.initialize = function() { diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index 931f1bbaf..6592fcdb7 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -222,10 +222,10 @@ var childLayout = me.getCardLayout(gridWidth, gridHeight, gridRows, gridCols, childRow, childCol, childRowspan, childColspan); - if($(this).is('.feed')) { + if($(this).is('.jamtrack')) { feedCardLayout = childLayout; } - else if($(this).is('.findsession')) { + else if($(this).is('.jamclass')) { findCardLayout = childLayout; } diff --git a/web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee b/web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee index 431f1ee98..fc68f432a 100644 --- a/web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/BookLesson.js.jsx.coffee @@ -17,7 +17,6 @@ UserStore = context.UserStore {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide}) onUserChanged: (userState) -> - console.log("userState", userState) @setState({user: userState?.user}) checkboxChanged: (e) -> diff --git a/web/app/assets/javascripts/scheduled_session.js.erb b/web/app/assets/javascripts/scheduled_session.js.erb index 11891e938..3296d9275 100644 --- a/web/app/assets/javascripts/scheduled_session.js.erb +++ b/web/app/assets/javascripts/scheduled_session.js.erb @@ -48,6 +48,10 @@ var $btnSelectFiles = null; var $selectedFilenames = null; var $uploadSpinner = null; + var $quickStartSoloBtn = null; + var $quickStartOpenBtn = null; + var $startOrScheduledBtn = null; + var $featureSessions = null; // Step1 layout var $screenStep1 = null; @@ -57,17 +61,19 @@ var $fetchingSpinner = null; var $fetchingSpinnerLabel = null; var $noSessionFound = null; + var $sessionHeader = null; var scheduledSessions = {}; // Step4 layout var $policyTypes = null; - var TOTAL_STEPS = 5; - var STEP_SELECT_TYPE = 0; - var STEP_SELECT_PLAYING = 1; - var STEP_SELECT_INVITE = 2; - var STEP_SELECT_POLICY = 3; - var STEP_SELECT_CONFIRM = 4; + var TOTAL_STEPS = 6; + var STEP_HOME = 0; + var STEP_SELECT_TYPE = 1; + var STEP_SELECT_PLAYING = 2; + var STEP_SELECT_INVITE = 3; + var STEP_SELECT_POLICY = 4; + var STEP_SELECT_CONFIRM = 5; var ONE_HOUR = 3600 * 1000; var ONE_MINUTE = 60 * 1000; @@ -92,6 +98,84 @@ function afterLoadScheduledSessions(sessionList) { + $featureSessions.empty() + + createSessionSettings.session_count = sessionList.length; + + if (createSessionSettings.session_count == 0) { + createSessionSettings.selectedSessionId = null; + $featureSessions.append('No sessions') + } + else { + $.each(sessionList, function (i, session) { + scheduledSessions[session.id] = session; + }); + + context._.each(sessionList, function (session) { + session.scheduled_start = new Date(session.scheduled_start).toDateString() + ', ' + + context.JK.formatUtcTime(new Date(session.scheduled_start), false); + + var time = session.pretty_scheduled_start_with_timezone; + var receivedUsers = {} + var sessionLink = '/client#/account/sessionDetail/' + session.id + var isActive = session.active_music_session && session.active_music_session.participants.length > 0 + + if(isActive) { + time = 'NOW' + + context._.each(session.active_music_session.participants, function(participant) { + receivedUsers[participant.id] = participant.name; + }) + + sessionLink = '/sessions/' + session.id + } + else { + + context._.each(session.approved_rsvps, function(approved_rsvp) { + receivedUsers[approved_rsvp.id] = approved_rsvp.name; + }) + + context._.each(session.invitations, function(invitation) { + receivedUsers[invitation.receiver_id] = invitation.receiver_name; + }) + } + var $row = $('' + session.pretty_scheduled_start_short + '') + var $sessionCell = $row.find('.session-link') + if (isActive) { + var $sessionLink = $('' + session.name + '') + $sessionCell.append($sessionLink) + context.JK.popExternalLinks($sessionCell) + } + else + { + var $sessionLink = $('' + session.name + '') + $sessionCell.append($sessionLink) + } + + var $actionsCell = $row.find('.actions'); + var $actionsLink = $('join') + $actionsCell.append($actionsLink) + + var msg = ''; + context._.each(receivedUsers, function(value, key) { + var url = '/client#/profile/' + key + msg += '' + value + ', ' + }) + + if (msg.length > 0) { + msg = msg.slice(0, -2) + } + var $usersCell = $row.find('.session-users') + $usersCell.append(msg) + + $featureSessions.append($row); + }); + } + } + + + function afterLoadScheduledSessions2(sessionList) { + createSessionSettings.session_count = sessionList.length; if (createSessionSettings.session_count == 0) { @@ -157,6 +241,30 @@ } } + function clickQuickStartSolo () { + + logger.debug("user clicked quick start solo") + createSessionSettings.createType = '<%= MusicSession::CREATE_TYPE_QUICK_START %>' + next(); + return false; + } + + function clickQuickStartPublic () { + + logger.debug("user clicked quick start public") + createSessionSettings.createType = '<%= MusicSession::CREATE_TYPE_QUICK_PUBLIC %>' + next(); + return false; + } + + function clickStartOrSchedule () { + + logger.debug("user clicked start or scheduled") + toggleCreateType(null, '<%= MusicSession::CREATE_TYPE_IMMEDIATE %>') + next(); + return false; + } + function afterLoadUserDetail(userDetail) { var userInstruments = []; $.each(userDetail.instruments, function(index, userInstrument) { @@ -180,23 +288,33 @@ }); } - function beforeShowStep1() { + function beforeShowStep0() { $scheduledSessions.empty(); $noSessionFound.hide(); $fetchingSpinner.show(); $fetchingSpinnerLabel.show(); rest.findScheduledSessions({}) - .done(afterLoadScheduledSessions) - .fail(app.ajaxError) - .always(function(response) { - $fetchingSpinner.hide(); - $fetchingSpinnerLabel.hide(); - }); + .done(afterLoadScheduledSessions) + .fail(app.ajaxError) + .always(function(response) { + $fetchingSpinner.hide(); + $fetchingSpinnerLabel.hide(); + }); rest.getUserDetail() - .done(afterLoadUserDetail) - .fail(app.ajaxError); + .done(afterLoadUserDetail) + .fail(app.ajaxError); + + + } + + + function beforeShowStep1() { + $scheduledSessions.empty(); + $noSessionFound.hide(); + $fetchingSpinner.show(); + $fetchingSpinnerLabel.show(); createSessionSettings.startDate = createSessionSettings.startDate || (new Date().toDateString()); @@ -362,6 +480,10 @@ $screen.find('#session-policy-disp').html(createSessionSettings.session_policy); } + function beforeMoveStep0() { + return true; + } + function beforeMoveStep1() { if (createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_START_SCHEDULED%>') { createSessionSettings.selectedSessionId = $scheduledSessions.find('.iradio_minimal.checked input[name="scheduled-session-info"]').attr('data-session-id'); @@ -427,6 +549,24 @@ createSessionSettings.recurring_mode.label = 'Not Recurring'; createSessionSettings.recurring_mode.value = 'once'; } + else if (createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_PUBLIC %>') { + createSessionSettings.genresValues = ['Pop']; + createSessionSettings.genres = ['pop']; + createSessionSettings.timezone.label = "(GMT-06:00) Central Time (US & Canada)"; + createSessionSettings.timezone.value = "Central Time (US & Canada),America/Chicago"; + createSessionSettings.name = "Open Session"; + createSessionSettings.description = "Feel free to join this session, it's open!"; + createSessionSettings.notations = []; + createSessionSettings.language.label = 'English'; + createSessionSettings.language.value = 'eng'; + createSessionSettings.session_policy = 'Standard'; + createSessionSettings.musician_access.label = "Musicians may join at will"; + createSessionSettings.musician_access.value = "musicians"; + createSessionSettings.fans_access.label = "Fans can listen to you while you're playing in session, and can text chat with each other while listening."; + createSessionSettings.fans_access.value = "listen-chat-each"; + createSessionSettings.recurring_mode.label = 'Not Recurring'; + createSessionSettings.recurring_mode.value = 'once'; + } else { createSessionSettings.startDate = $screen.find('#session-start-date').val(); createSessionSettings.startTime = $startTimeList.val(); @@ -660,7 +800,7 @@ data.legal_terms = true; data.language = createSessionSettings.language.value; data.band = createSessionSettings.band.value; - if (createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_START %>' || createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_IMMEDIATE %>') { + if (createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_START %>' || createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_IMMEDIATE %>' || createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_PUBLIC %>') { data.start = new Date().toDateString() + ' ' + context.JK.formatUtcTime(new Date(), false); data.duration = "60"; } @@ -748,7 +888,7 @@ $('#create-session-buttons .btn-next').off('click'); var newSessionId = response.id; - if (createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_START %>' || createSessionSettings.createType == "immediately") { + if (createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_START %>' || createSessionSettings.createType == "immediately" || createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_PUBLIC %>') { joinSession(newSessionId); } else { @@ -766,22 +906,26 @@ var STEPS = { 0: { + beforeShow: beforeShowStep0, + beforeMove: beforeMoveStep0 + }, + 1: { beforeShow: beforeShowStep1, beforeMove: beforeMoveStep1 }, - 1: { + 2: { beforeShow: beforeShowStep2, beforeMove: beforeMoveStep2 }, - 2: { + 3: { beforeShow: beforeShowStep3, beforeMove: beforeMoveStep3 }, - 3: { + 4: { beforeShow: beforeShowStep4, beforeMove: beforeMoveStep4 }, - 4: { + 5: { beforeShow: beforeShowStep5, beforeMove: beforeMoveStep5 } @@ -850,6 +994,12 @@ $createSessionSteps.find("#todo-steps").append($("
")); $screen.find('#create-session-steps').replaceWith($createSessionSteps); + if (step == 0) { + $sessionHeader.hide() + } + else { + $sessionHeader.show() + } beforeShowStep(); // update buttons @@ -862,6 +1012,10 @@ // hide back button if 1st step or last step if (step == 0) { $btnBack.hide(); + $btnNext.hide(); + } + else { + $btnNext.show(); } if (step == STEP_SELECT_TYPE && createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_START_SCHEDULED%>' && createSessionSettings.selectedSessionId == null) { @@ -907,14 +1061,16 @@ function willOptionStartSession() { return createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_START_SCHEDULED%>' || createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_IMMEDIATE %>' || - createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_START %>'; + createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_START %>' || + createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_PUBLIC%>'; } function optionRequiresMultiplayerProfile() { return createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_START_SCHEDULED%>' || createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_IMMEDIATE %>' || createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_RSVP %>' || - createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_SCHEDULE_FUTURE %>'; + createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_SCHEDULE_FUTURE %>' || + createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_PUBLIC %>'; } function next(event) { @@ -923,7 +1079,7 @@ return false; } - if(createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_START %>') { + if(createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_START %>' || createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_PUBLIC %>') { // short-cut added for private sessions; just get it going beforeMoveStep1(); // this will populate the createSessionSettings structure startSessionClicked(); // and this will create the session @@ -939,6 +1095,7 @@ var valid = beforeMoveStep(); if (!valid) { + console.log("beforeMoveStep check invalid") return false; } @@ -947,7 +1104,7 @@ } if ($(this).is('.disabled')) return false; - if ($.inArray(createSessionSettings.createType, ['<%= MusicSession::CREATE_TYPE_START_SCHEDULED%>', '<%= MusicSession::CREATE_TYPE_QUICK_START %>']) > -1) + if ($.inArray(createSessionSettings.createType, ['<%= MusicSession::CREATE_TYPE_START_SCHEDULED%>', '<%= MusicSession::CREATE_TYPE_QUICK_START %>', '<%= MusicSession::CREATE_TYPE_QUICK_PUBLIC %>']) > -1) step = STEP_SELECT_CONFIRM; else step++; @@ -1094,6 +1251,8 @@ context.JK.helpBubble($sessionPlusMusiciansLabel, 'session-plus-musicians', {}, {offsetParent: $sessionPlusMusiciansLabel.closest('.content-wrapper')}); $editScheduledSessions.on('click', onEditSessions); + + context.JK.popExternalLinks($screen) } function changeSelectedFiles() { @@ -1144,19 +1303,19 @@ if ($.inArray(createSessionSettings.createType, ['<%= MusicSession::CREATE_TYPE_START_SCHEDULED%>', '<%= MusicSession::CREATE_TYPE_QUICK_START %>']) > -1) { if (step == STEP_SELECT_CONFIRM) { - for (var i = 1; i < 4; i++) { - $screen.find('#create-session-steps .session-stepnumber[data-step-number="' + i + '"]').hide(); - } - $screen.find('#create-session-steps .session-stepnumber[data-step-number="4"]').html("2"); - $screen.find('#create-session-steps .session-stepnumber[data-step-number="0"]').on('click', back); - $screen.find('#create-session-steps .session-stepnumber[data-step-number="0"]').addClass('session-stephover'); - } - else if (step == STEP_SELECT_TYPE) { for (var i = 2; i < 5; i++) { $screen.find('#create-session-steps .session-stepnumber[data-step-number="' + i + '"]').hide(); } - $screen.find('#create-session-steps .session-stepnumber[data-step-number="4"]').html("5"); - var $nextStep = $screen.find('#create-session-steps .session-stepnumber[data-step-number="1"]'); + $screen.find('#create-session-steps .session-stepnumber[data-step-number="5"]').html("2"); + $screen.find('#create-session-steps .session-stepnumber[data-step-number="1"]').on('click', back); + $screen.find('#create-session-steps .session-stepnumber[data-step-number="1"]').addClass('session-stephover'); + } + else if (step == STEP_SELECT_TYPE) { + for (var i = 3; i < 6; i++) { + $screen.find('#create-session-steps .session-stepnumber[data-step-number="' + i + '"]').hide(); + } + $screen.find('#create-session-steps .session-stepnumber[data-step-number="5"]').html("5"); + var $nextStep = $screen.find('#create-session-steps .session-stepnumber[data-step-number="2"]'); if (createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_START %>') { $nextStep.on('click', next); $nextStep.addClass('session-stephover') @@ -1176,10 +1335,12 @@ } } - function toggleCreateType(event) { + function toggleCreateType(event, checkedType) { - var $checkedType = $(event.target); - var checkedType = $checkedType.attr('info-value'); + if(!checkedType) { + var $checkedType = $(event.target); + var checkedType = $checkedType.attr('info-value'); + } if (checkedType == createSessionSettings.createType) return; @@ -1293,6 +1454,10 @@ $inputFiles.on('change', changeSelectedFiles); $btnSelectFiles.on('click', toggleSelectFiles); + + $quickStartSoloBtn.on('click', clickQuickStartSolo) + $quickStartOpenBtn.on('click', clickQuickStartPublic) + $startOrScheduledBtn.on('click', clickStartOrSchedule) } function initialize(invitationDialogInstance, friendSelectorDialog, instrumentSelectorInstance, instrumentRSVPSelectorInstance) { @@ -1332,6 +1497,11 @@ $fetchingSpinner = $screen.find('#fetching-spinner'); $fetchingSpinnerLabel = $screen.find('#fetching-spinner-label'); $noSessionFound = $screen.find("#scheduled-session-not-found"); + $sessionHeader = $screen.find('.session-header') + $quickStartSoloBtn = $screen.find('.quick-start-solo') + $quickStartOpenBtn = $screen.find('.quick-start-open') + $startOrScheduledBtn = $screen.find('.start-or-schedule') + $featureSessions = $screen.find('.featured-sessions tbody') initializeControls(); events(); diff --git a/web/app/assets/stylesheets/client/createSession.css.scss b/web/app/assets/stylesheets/client/createSession.css.scss index d324aca04..fbd3a981d 100644 --- a/web/app/assets/stylesheets/client/createSession.css.scss +++ b/web/app/assets/stylesheets/client/createSession.css.scss @@ -17,6 +17,93 @@ font-size: 13px; } + .column { + h2 { + margin-bottom:20px; + } + &.column-left { + width:60%; + float:left; + padding-right:10px; + @include border_box_sizing; + } + &.column-right{ + width:40%; + float:left; + padding-left:10px; + @include border_box_sizing; + + p { + margin:0; + font-size: 12px; + line-height: 125%; + width:100%; + @include border_box_sizing; + } + a.button-orange { + margin:0 0 15px 3px; + } + } + } + .find-sessions, .view-the-feed { + margin-bottom:20px; + } + .view-the-feed { + margin-top:30px; + } + .view-the-feed-learn-more, .find-session-learn-more { + margin-top:10px; + font-size:12px; + } + .quick-option { + position:relative; + margin-bottom:20px; + } + .quick-options { + margin: 0 0 35px 0; + a { + width:130px; + margin-left:0px; + position:absolute; + top:5px; + left: 0; + } + p { + margin:0; + font-size: 12px; + line-height: 125%; + width:100%; + padding-left:176px; + @include border_box_sizing; + } + a.schedule-learn-more { + position:static; + margin-left:176px; + font-size:12px; + padding-top:12px; + } + } + .learn-more-sessions-header { + margin-top:40px; + } + .learn-more-sessions { + a { + display:block; + font-size:12px; + margin-bottom:3px; + } + p { + margin-bottom:20px !important; + } + } + table.featured-sessions { + .actions { + text-align:center; + } + .session-users { + a { color:#fc0; } + } + } .session-header { padding: 15px 35px; .session-stepnumber { @@ -101,7 +188,7 @@ padding-left:5px; } - #session-step-1 { + #session-step-2 { ul#create-session-type { margin-left: 0px; list-style: none; @@ -143,7 +230,7 @@ } } - #session-step-2 { + #session-step-3 { #select-genre { padding-bottom: 10px; @@ -200,7 +287,7 @@ } } - #session-step-3 { + #session-step-4 { .session-instrumentlist { padding: 10px; @@ -275,7 +362,7 @@ } } - #session-step-4 { + #session-step-5 { .terms-checkbox { float:left; @@ -305,7 +392,7 @@ } - #session-step-5 { + #session-step-6 { } #session-name-disp { diff --git a/web/app/views/clients/_home.html.slim b/web/app/views/clients/_home.html.slim index 14b7961de..e6d0e0d15 100644 --- a/web/app/views/clients/_home.html.slim +++ b/web/app/views/clients/_home.html.slim @@ -1,81 +1,64 @@ .screen.no-login-required layout="screen" layout-id="home" / Layout is different if jam_tracks tile available: - -jamtracks=Rails.configuration.jam_tracks_available || (current_user && current_user.admin) - -if (jamtracks) - -small_tile_size="2.4" - -column_positions=["0.0,1", "2.4,1", "4.8,1", "7.2,1", "9.6,1"] - -else - -small_tile_size="3.0" - -column_positions=["0,1", "3,1", "0,1", "6,1", "9,1"] / Grid is the count of the smallest spaces, then / individual spells span those spaces -if @nativeClient .grid layout-grid="2x12" .homecard.createsession layout-grid-columns="4" layout-grid-position="0,0" layout-grid-rows="1" layout-link="createSession" type="createSession" class="#{logged_in_not_logged_in_class}" - h2 create session + h2 sessions .homebox-info /! 4 friends online, 2 currently in sessions - .homecard.findsession layout-grid-columns="4" layout-grid-position="4,0" layout-grid-rows="1" layout-link="findSession" type="findSession" class="#{logged_in_not_logged_in_class}" - h2 find session + .homecard.jamclass layout-grid-columns="4" layout-grid-position="4,0" layout-grid-rows="1" layout-link="jamclass" type="jamclass" class="#{logged_in_not_logged_in_class}" + h2 jamclass .homebox-info /! 1 session invitation, 19 public sessions active - .homecard.feed layout-grid-columns="4" layout-grid-position="8,0" layout-grid-rows="1" layout-link="feed" class="#{logged_in_not_logged_in_class}" - h2 feed + .homecard.jamtrack layout-grid-columns="4" layout-grid-position="8,0" layout-grid-rows="1" layout-link="jamtrack" type="jamtrack" + h2 jamtracks .homebox-info - /! 4 friends online, 2 currently in sessions - .homecard.musicians layout-grid-columns=small_tile_size layout-grid-position=column_positions[0] layout-grid-rows="1" layout-link="musicians" class="#{logged_in_not_logged_in_class}" + /! 5 followers, 3 following + .homecard.musicians layout-grid-columns="3" layout-grid-position="0,1" layout-grid-rows="1" layout-link="musicians" class="#{logged_in_not_logged_in_class}" h2 musicians .homebox-info /! 5 followers, 3 following - .homecard.bands layout-grid-columns=small_tile_size layout-grid-position=column_positions[1] layout-grid-rows="1" layout-link="bands" class="#{logged_in_not_logged_in_class}" + .homecard.bands layout-grid-columns="3" layout-grid-position="3,1" layout-grid-rows="1" layout-link="bands" class="#{logged_in_not_logged_in_class}" h2 bands .homebox-info /! 1 session invitation, 19 public sessions active - -if jamtracks - .homecard.jamtrack layout-grid-columns=small_tile_size layout-grid-position=column_positions[2] layout-grid-rows="1" layout-link="jamtrack" - h2 jamtracks - .homebox-info - /! 5 followers, 3 following - .homecard.profile layout-grid-columns=small_tile_size layout-grid-position=column_positions[3] layout-grid-rows="1" class="#{logged_in_not_logged_in_class}" + .homecard.profile layout-grid-columns="3" layout-grid-position="6,1" layout-grid-rows="1" class="#{logged_in_not_logged_in_class}" h2 profile .homebox-info /! 5 followers, 3 following - .homecard.account layout-grid-columns=small_tile_size layout-grid-position=column_positions[4] layout-grid-rows="1" layout-link="account" class="#{logged_in_not_logged_in_class}" + .homecard.account layout-grid-columns="3" layout-grid-position="9,1" layout-grid-rows="1" layout-link="account" class="#{logged_in_not_logged_in_class}" h2 account .homebox-info /! free service level -else .grid layout-grid="2x12" .homecard.createsession layout-grid-columns="4" layout-grid-position="0,0" layout-grid-rows="1" layout-link="createSession" type="createSession" class="#{logged_in_not_logged_in_class}" - h2 create session + h2 sessions .homebox-info - /! 4 friends online, 2 currently in sessions - .homecard.findsession layout-grid-columns="4" layout-grid-position="4,0" layout-grid-rows="1" layout-link="findSession" type="findSession" class="#{logged_in_not_logged_in_class}" - h2 find session + /! 4 friends online, 2 currently in sessions + .homecard.jamclass layout-grid-columns="4" layout-grid-position="4,0" layout-grid-rows="1" layout-link="jamclass" type="jamclass" class="#{logged_in_not_logged_in_class}" + h2 jamclass .homebox-info - /! 1 session invitation, 19 public sessions active - .homecard.feed layout-grid-columns="4" layout-grid-position="8,0" layout-grid-rows="1" layout-link="feed" class="#{logged_in_not_logged_in_class}" - h2 feed + /! 1 session invitation, 19 public sessions active + .homecard.jamtrack layout-grid-columns="4" layout-grid-position="8,0" layout-grid-rows="1" layout-link="jamtrack" type="jamtrack" + h2 jamtracks .homebox-info - /! 4 friends online, 2 currently in sessions - .homecard.musicians layout-grid-columns=small_tile_size layout-grid-position=column_positions[0] layout-grid-rows="1" layout-link="musicians" class="#{logged_in_not_logged_in_class}" + /! 5 followers, 3 following + .homecard.musicians layout-grid-columns="3" layout-grid-position="0,1" layout-grid-rows="1" layout-link="musicians" class="#{logged_in_not_logged_in_class}" h2 musicians .homebox-info - /! 5 followers, 3 following - .homecard.bands layout-grid-columns=small_tile_size layout-grid-position=column_positions[1] layout-grid-rows="1" layout-link="bands" class="#{logged_in_not_logged_in_class}" + /! 5 followers, 3 following + .homecard.bands layout-grid-columns="3" layout-grid-position="3,1" layout-grid-rows="1" layout-link="bands" class="#{logged_in_not_logged_in_class}" h2 bands .homebox-info - -if jamtracks - /! 1 session invitation, 19 public sessions active - .homecard.jamtrack layout-grid-columns=small_tile_size layout-grid-position=column_positions[2] layout-grid-rows="1" layout-link="jamtrack" - h2 jamtracks - .homebox-info - /! 5 followers, 3 following - .homecard.profile layout-grid-columns=small_tile_size layout-grid-position=column_positions[3] layout-grid-rows="1" class="#{logged_in_not_logged_in_class}" + /! 1 session invitation, 19 public sessions active + .homecard.profile layout-grid-columns="3" layout-grid-position="6,1" layout-grid-rows="1" class="#{logged_in_not_logged_in_class}" h2 profile .homebox-info - /! 5 followers, 3 following - .homecard.account layout-grid-columns=small_tile_size layout-grid-position=column_positions[4] layout-grid-rows="1" layout-link="account" class="#{logged_in_not_logged_in_class}" + /! 5 followers, 3 following + .homecard.account layout-grid-columns="3" layout-grid-position="9,1" layout-grid-rows="1" layout-link="account" class="#{logged_in_not_logged_in_class}" h2 account .homebox-info /! free service level diff --git a/web/app/views/clients/_scheduledSession.html.erb b/web/app/views/clients/_scheduledSession.html.erb index 9e8e2d85a..abcc7b260 100644 --- a/web/app/views/clients/_scheduledSession.html.erb +++ b/web/app/views/clients/_scheduledSession.html.erb @@ -6,7 +6,7 @@ <%= image_tag "content/icon_add.png", :size => "19x19" %>
-

create session

+

sessions

<%= render "screen_navigation" %>
@@ -21,19 +21,60 @@
+
+

start a session

+
+
QUICK START SOLO

Use this button to quick start a private session just for yourself. Good for solo practice and gear testing.

+
QUICK START OPEN

Use this button to quick start an open session that anyone can join to play with you, and that fans can listen to.

+
START OR SCHEDULE

Use this button start a wizard that will let you have more control over your session. Schedule future sessions. Start private sessions. Invite other musicians. And lots more...

learn more
+
+

featured sessions

+ + + + + + + + +
+
+

check out other sessions

+ +
+ FIND SESSIONS +

+ Use the Find Sessions feature to browse through current and future/scheduled sessions you can join to play with other musicians. +

+ learn more +
+ +
+ VIEW THE FEED +

+ Use the Feed to check out current and past sessions and session recordings, and to listen to current sessions. +

+ learn more +
+ +

learn more about online sessions

+ + +
+
+

When do you want to schedule and play in this session?


    -
  • - - -
    -
  • -
  • - - -
    -
@@ -159,7 +193,7 @@
-
+

Please select a genre for your session:

@@ -221,7 +255,7 @@
-
+
@@ -266,7 +300,7 @@
-
+
    @@ -380,7 +414,7 @@
-
+

When are you starting your session?

@@ -435,15 +469,15 @@