This commit is contained in:
Seth Call 2016-09-26 21:56:12 -05:00
parent eb89ea0a43
commit 3117c7ed3e
31 changed files with 779 additions and 106 deletions

View File

@ -367,3 +367,4 @@ non_free_jamtracks.sql
retailers.sql
second_ed.sql
second_ed_v2.sql
retailers_v2.sql

3
db/up/retailers_v2.sql Normal file
View File

@ -0,0 +1,3 @@
ALTER TABLE lesson_bookings ADD COLUMN posa_card_id VARCHAR(64);
ALTER TABLE jam_track_rights ADD COLUMN posa_card_id VARCHAR(64);
ALTER TABLE lesson_package_purchases ADD COLUMN posa_card_id VARCHAR(64);

View File

@ -1038,7 +1038,20 @@ module JamRuby
@user = lesson_session.student
email = @student.email
subject = "You have used #{@student.used_test_drives} of #{@student.total_test_drives} TestDrive lesson credits"
if lesson_session.posa_card
@total_credits = @student.total_posa_credits
@used_credits = @student.used_posa_credits
@remaining_credits = @student.jamclass_credits
else
@total_credits = @student.total_test_drives
@used_credits = @student.used_test_drives
@remaining_credits = @student.remaining_test_drives
end
subject = "You have used #{@used_credits} of #{@total_credits} TestDrive lesson credits"
unique_args = {:type => "student_test_drive_success"}
sendgrid_category "Notification"

View File

@ -1,4 +1,4 @@
<% provide(:title, "You have used #{@student.used_test_drives} of #{@student.total_test_drives} TestDrive lesson credits") %>
<% provide(:title, "You have used #{@used_credits} of #{@total_credits} TestDrive lesson credits") %>
<% provide(:photo_url, @teacher.resolved_photo_url) %>
<% content_for :note do %>
@ -7,7 +7,7 @@
</p>
<p>We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have
used <%= @student.used_test_drives %> TestDrive credits, and you have <%= @student.remaining_test_drives %>
used <%= @used_credits %> TestDrive credits, and you have <%= @remaining_credits %>
remaining TestDrive lesson(s) available. If you havent booked your next TestDrive lesson,
<a href="<%= User.search_url %>" style="color:#fc0">click here</a> to search our teachers and get your next
lesson lined up today!</p>

View File

@ -1,4 +1,4 @@
You have used <%= @student.used_test_drives %> of <%= @student.total_test_drives %> TestDrive lesson credits.
You have used <%= @used_credits %> of <%= @total_credits %> TestDrive lesson credits.
<% if @student.has_rated_teacher(@teacher) %>
Also, please rate your teacher at <%= @teacher.ratings_url %> now for todays lesson to help other students in the community find the best instructors.

View File

@ -221,8 +221,13 @@ module JamRuby
if is_single_free?
user.remaining_free_lessons = user.remaining_free_lessons - 1
elsif is_test_drive?
if posa_card
user.jamclass_credits = user.jamclass_credits - 1
else
user.remaining_test_drives = user.remaining_test_drives - 1
end
end
user.save(validate: false)
end
end
@ -762,7 +767,17 @@ module JamRuby
lesson_booking.payment_style = payment_style
lesson_booking.description = description
lesson_booking.status = STATUS_REQUESTED
if lesson_type == LESSON_TYPE_TEST_DRIVE
# if the user has any jamclass credits, then we should get their most recent posa purchase
if user.jamclass_credits > 0
lesson_booking.posa_card = most_recent_posa_purchase.posa_card
else
# otherwise, it's a normal test drive, and we should honor test_drive_package_choice if specified
lesson_booking.test_drive_package_choice = test_drive_package_choice
end
end
if lesson_booking.teacher && lesson_booking.teacher.teacher.school
lesson_booking.school = lesson_booking.teacher.teacher.school
end

View File

@ -15,6 +15,7 @@ module JamRuby
belongs_to :teacher, class_name: "JamRuby::User"
belongs_to :lesson_booking, class_name: "JamRuby::LessonBooking"
belongs_to :lesson_payment_charge, class_name: "JamRuby::LessonPaymentCharge", foreign_key: :charge_id
belongs_to :posa_card, class_name: "JamRuby::PosaCard", foreign_key: :posa_card_id
has_one :lesson_session, class_name: "JamRuby::LessonSession", dependent: :destroy
has_many :teacher_distributions, class_name: "JamRuby::TeacherDistribution"
@ -30,6 +31,10 @@ module JamRuby
def validate_test_drive
if user
# if this is a posa card purchase, we won't stop it from getting created
if posa_card_id
return
end
if lesson_package_type.is_test_drive? && !user.can_buy_test_drive?
errors.add(:user, "can not buy test drive right now because you have already purchased it within the last year")
end
@ -56,6 +61,11 @@ module JamRuby
end
def add_test_drives
if posa_card_id
#user.jamclass_credits incremented in posa_card.rb
return
end
if self.lesson_package_type.is_test_drive?
new_test_drives = user.remaining_test_drives + lesson_package_type.test_drive_count
User.where(id: user.id).update_all(remaining_test_drives: new_test_drives)
@ -75,17 +85,19 @@ module JamRuby
lesson_payment_charge.amount_in_cents / 100.0
end
def self.create(user, lesson_booking, lesson_package_type, year = nil, month = nil)
def self.create(user, lesson_booking, lesson_package_type, year = nil, month = nil, posa_card = nil)
purchase = LessonPackagePurchase.new
purchase.user = user
purchase.lesson_booking = lesson_booking
purchase.teacher = lesson_booking.teacher if lesson_booking
purchase.posa_card = posa_card
if year
purchase.year = year
purchase.month = month
purchase.recurring = true
# this is for monthly
if lesson_booking && lesson_booking.requires_teacher_distribution?(purchase)
teacher_dist = TeacherDistribution.create_for_lesson_package_purchase(purchase, false)
purchase.teacher_distributions << teacher_dist

View File

@ -11,7 +11,7 @@ module JamRuby
@@log = Logging.logger[LessonSession]
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, :is_monthly_payment?, :school_on_school?, :school_on_school_payment?, :no_school_on_school_payment?, :payment_if_school_on_school?, :scheduling_email, :teacher_school_emails, :school_and_teacher, :school_over_teacher, :school_and_teacher_ids, :school_over_teacher_ids, to: :lesson_booking
delegate :is_test_drive?, :is_single_free?, :is_normal?, :approved_before?, :is_active?, :recurring, :is_monthly_payment?, :school_on_school?, :school_on_school_payment?, :no_school_on_school_payment?, :payment_if_school_on_school?, :scheduling_email, :teacher_school_emails, :school_and_teacher, :school_over_teacher, :school_and_teacher_ids, :school_over_teacher_ids, :posa_card, to: :lesson_booking
delegate :pretty_scheduled_start, to: :music_session
@ -581,9 +581,13 @@ module JamRuby
lesson_session.slot = booking.default_slot
lesson_session.assigned_student = booking.student
lesson_session.user = booking.student
if booking.is_test_drive? && booking.student.remaining_test_drives > 0
if booking.is_test_drive?
if booking.student.jamclass_credits > 0
lesson_session.lesson_package_purchase = booking.student.most_recent_posa_purchase
elsif booking.student.remaining_test_drives > 0
lesson_session.lesson_package_purchase = booking.student.most_recent_test_drive_purchase
end
end
lesson_session.save
if lesson_session.errors.any?
@ -739,8 +743,12 @@ module JamRuby
# 1st time this has ever been approved; there are other things we need to do
if lesson_package_purchase.nil? && lesson_booking.is_test_drive?
if student.jamclass_credits > 0
self.lesson_package_purchase = student.most_recent_posa_purchase
elsif student.remaining_test_drives > 0
self.lesson_package_purchase = student.most_recent_test_drive_purchase
end
end
if self.save
# also let the lesson_booking know we got accepted

View File

@ -31,6 +31,18 @@ module JamRuby
validate :must_be_activated
validate :within_one_year
def credits
if card_type == JAM_TRACKS_5
5
elsif card_type == JAM_TRACKS_10
10
elsif card_type == JAM_CLASS_4
4
else
raise "unknown card type #{card_type}"
end
end
def already_activated
if activated_at && activated_at_was && activated_at_changed?
if retailer && retailer_id == retailer_id_was

View File

@ -23,7 +23,7 @@ module JamRuby
validates_length_of :password, minimum: 6, maximum: 100, :if => :should_validate_password
after_create :create_affiliate
before_save :stringify_avatar_info, :if => :updating_avatar
# before_save :stringify_avatar_info, :if => :updating_avatar
def create_affiliate
AffiliatePartner.create_from_retailer(self)
@ -77,12 +77,12 @@ module JamRuby
cropped_large_s3_path = cropped_large_fpfile["key"]
self.update_attributes(
:original_fpfile => original_fpfile,
:cropped_fpfile => cropped_fpfile,
:cropped_large_fpfile => cropped_large_fpfile,
:original_fpfile => original_fpfile.to_json,
:cropped_fpfile => cropped_fpfile.to_json,
:cropped_large_fpfile => cropped_large_fpfile.to_json,
:cropped_s3_path => cropped_s3_path,
:cropped_large_s3_path => cropped_large_s3_path,
:crop_selection => crop_selection,
:crop_selection => crop_selection.to_json,
:photo_url => S3Util.url(aws_bucket, escape_filename(cropped_s3_path), :secure => true),
:large_photo_url => S3Util.url(aws_bucket, escape_filename(cropped_large_s3_path), :secure => true)
)
@ -116,9 +116,9 @@ module JamRuby
# so we need t oconvert it to JSON before storing it (otherwise it gets serialized as a ruby object)
# later, when serving this data out to the REST API, we currently just leave it as a string and make a JSON capable
# client parse it, because it's very rare when it's needed at all
self.original_fpfile = original_fpfile.to_json if !original_fpfile.nil?
self.cropped_fpfile = cropped_fpfile.to_json if !cropped_fpfile.nil?
self.crop_selection = crop_selection.to_json if !crop_selection.nil?
self.original_fpfile = original_fpfile.to_json if !original_fpfile.nil? && original_fpfile.class != String
self.cropped_fpfile = cropped_fpfile.to_json if !cropped_fpfile.nil? && cropped_fpfile.class != String
self.crop_selection = crop_selection.to_json if !crop_selection.nil? && crop_selection.class != String
end
end
end

View File

@ -30,7 +30,7 @@ module JamRuby
validate :validate_avatar_info
after_create :create_affiliate
before_save :stringify_avatar_info, :if => :updating_avatar
#before_save :stringify_avatar_info, :if => :updating_avatar
def is_education?
education
@ -84,12 +84,12 @@ module JamRuby
cropped_large_s3_path = cropped_large_fpfile["key"]
self.update_attributes(
:original_fpfile => original_fpfile,
:cropped_fpfile => cropped_fpfile,
:cropped_large_fpfile => cropped_large_fpfile,
:original_fpfile => original_fpfile.to_json,
:cropped_fpfile => cropped_fpfile.to_json,
:cropped_large_fpfile => cropped_large_fpfile.to_json,
:cropped_s3_path => cropped_s3_path,
:cropped_large_s3_path => cropped_large_s3_path,
:crop_selection => crop_selection,
:crop_selection => crop_selection.to_json,
:photo_url => S3Util.url(aws_bucket, escape_filename(cropped_s3_path), :secure => true),
:large_photo_url => S3Util.url(aws_bucket, escape_filename(cropped_large_s3_path), :secure => true)
)

View File

@ -219,7 +219,18 @@ module JamRuby
teacher.teaches_test_drive = params[:teaches_test_drive] if params.key?(:teaches_test_drive)
teacher.test_drives_per_week = params[:test_drives_per_week] if params.key?(:test_drives_per_week)
teacher.test_drives_per_week = 10 if !params.key?(:test_drives_per_week) # default to 10 in absence of others
teacher.school_id = params[:school_id] if params.key?(:school_id)
if params.key?(:school_id)
teacher.school_id = params[:school_id]
if !teacher.joined_school_at
teacher.joined_school_at = Time.now
end
end
if params.key?(:retailer_id)
teacher.retailer_id = params[:retailer_id]
if !teacher.joined_retailer_at
teacher.joined_retailer_at = Time.now
end
end
# How to validate:
teacher.validate_introduction = !!params[:validate_introduction]

View File

@ -1136,6 +1136,8 @@ module JamRuby
teacher = options[:teacher]
school_invitation_code = options[:school_invitation_code]
school_id = options[:school_id]
retailer_invitation_code = options[:retailer_invitation_code]
retailer_id = options[:retailer_id]
school_interest = options[:school_interest]
education_interest = options[:education_interest]
origin = options[:origin]
@ -1144,6 +1146,7 @@ module JamRuby
test_drive_package = TestDrivePackage.find_by_name(test_drive_package_details[:name]) if test_drive_package_details
school = School.find(school_id) if school_id
retailer = School.find(retailer_id) if retailer_id
user = User.new
user.validate_instruments = true
UserManager.active_record_transaction do |user_manager|
@ -1158,6 +1161,16 @@ module JamRuby
end
end
if retailer_invitation_code
retailer_invitation = RetailerInvitation.find_by_invitation_code(retailer_invitation_code)
if retailer_invitation
first_name ||= retailer_invitation.first_name
last_name ||= retailer_invitation.last_name
retailer_invitation.accepted = true
retailer_invitation.save
end
end
user.first_name = first_name if first_name.present?
user.last_name = last_name if last_name.present?
user.email = email
@ -1195,10 +1208,18 @@ module JamRuby
user.affiliate_referral = school.affiliate_partner
elsif user.is_a_teacher
school = School.find_by_id(school_id)
school_name = school ? school.name : 'a music school'
user.teacher = Teacher.build_teacher(user, validate_introduction: true, biography: "Empty biography", school_id: school_id)
user.affiliate_referral = school.affiliate_partner
end
elsif retailer_id.present?
if user.is_a_student
user.retailer_id = school_id
user.affiliate_referral = retailer.affiliate_partner
elsif user.is_a_teacher
retailer = Retailer.find_by_id(retailer_id)
user.teacher = Teacher.build_teacher(user, validate_introduction: true, biography: "Empty biography", retailer_id: retailer_id)
user.affiliate_referral = retailer.affiliate_partner
end
else
if user.is_a_teacher
user.teacher = Teacher.build_teacher(user, validate_introduction: true, biography: "Empty biography")
@ -2003,6 +2024,10 @@ module JamRuby
remaining_test_drives > 0
end
def has_posa_credits?
jamclass_credits > 0
end
def has_unprocessed_test_drives?
!unprocessed_test_drive.nil?
end
@ -2185,6 +2210,10 @@ module JamRuby
LessonBooking.unprocessed(self).where(lesson_type: LessonBooking::LESSON_TYPE_PAID).first
end
def most_recent_posa_purchase
lesson_purchases.where('lesson_package_type_id in (?)', LessonPackageType.test_drive_package_ids).where('posa_card_id is not null').order('created_at desc').first
end
def most_recent_test_drive_purchase
lesson_purchases.where('lesson_package_type_id in (?)', LessonPackageType.test_drive_package_ids).order('created_at desc').first
end
@ -2198,8 +2227,18 @@ module JamRuby
end
end
def total_posa_credits
purchase = most_recent_posa_purchase
if purchase
purchase.posa_card.credits
else
0
end
end
def test_drive_succeeded(lesson_session)
if self.remaining_test_drives <= 0
if (lesson_session.posa_card && self.jamclass_credits <= 0) || self.remaining_test_drives <= 0
UserMailer.student_test_drive_lesson_done(lesson_session).deliver_now
UserMailer.teacher_lesson_completed(lesson_session).deliver_now
else
@ -2211,7 +2250,13 @@ module JamRuby
def test_drive_declined(lesson_session)
# because we decrement test_drive credits as soon as you book, we need to bring it back now
if lesson_session.lesson_booking.user_decremented
if lesson_session.posa_card
self.jamclass_credits = self.jamclass_credits + 1
else
self.remaining_test_drives = self.remaining_test_drives + 1
end
self.save(validate: false)
end
@ -2221,7 +2266,12 @@ module JamRuby
if lesson_session.lesson_booking.user_decremented
# because we decrement test_drive credits as soon as you book, we need to bring it back now
if lesson_session.posa_card
self.jamclass_credits = self.jamclass_credits + 1
else
self.remaining_test_drives = self.remaining_test_drives + 1
end
self.save(validate: false)
end
UserMailer.teacher_test_drive_no_bill(lesson_session).deliver_now
@ -2232,6 +2282,10 @@ module JamRuby
total_test_drives - remaining_test_drives
end
def used_posa_credits
total_posa_credits - jamclass_credits
end
def uncollectables(limit = 10)
LessonPaymentCharge.where(user_id:self.id).order(:created_at).where('billing_attempts > 0').where(billed: false).limit(limit)
end

View File

@ -76,6 +76,7 @@
isNativeClient: gon.isNativeClient,
musician: context.JK.currentUserMusician,
sales_count: userDetail.sales_count,
owned_retailer_id: userDetail.owned_retailer_id,
is_affiliate_partner: userDetail.is_affiliate_partner,
affiliate_earnings: (userDetail.affiliate_earnings / 100).toFixed(2),
affiliate_referral_count: userDetail.affiliate_referral_count,
@ -146,6 +147,7 @@
$("#account-content-scroller").on('click', '#account-payment-history-link', function(evt) {evt.stopPropagation(); navToPaymentHistory(); return false; } );
$("#account-content-scroller").on('click', '#account-affiliate-partner-link', function(evt) {evt.stopPropagation(); navToAffiliates(); return false; } );
$("#account-content-scroller").on('click', '#account-school-link', function(evt) {evt.stopPropagation(); navToSchool(); return false; } );
$("#account-content-scroller").on('click', '#account-retailer-link', function(evt) {evt.stopPropagation(); navToRetailer(); return false; } );
}
function renderAccount() {
@ -208,6 +210,11 @@
window.location = '/client#/account/school'
}
function navToRetailer() {
resetForm()
window.location = '/client#/account/retailer'
}
// handle update avatar event
function updateAvatar(avatar_url) {
var photoUrl = context.JK.resolveAvatarUrl(avatar_url);

View File

@ -64,6 +64,7 @@
$screen.find('select[name=skill_level]').val(userDetail.skill_level);
$screen.find('select[name=concert_count]').val(userDetail.concert_count);
$screen.find('select[name=studio_session_count]').val(userDetail.studio_session_count);
context.JK.checkbox($instrumentSelector.find('input[type="checkbox"]'), true)
}
function isUserInstrument(instrument, userInstruments) {
@ -101,6 +102,8 @@
});
$userGenres.append(genreHtml);
});
context.JK.checkbox($userGenres.find('input[type="checkbox"]'), true)
});
}

View File

@ -3,8 +3,8 @@ rest = context.JK.Rest()
logger = context.JK.logger
AppStore = context.AppStore
SchoolActions = context.RetailerActions
SchoolStore = context.RetailerStore
RetailerActions = context.RetailerActions
RetailerStore = context.RetailerStore
UserStore = context.UserStore
profileUtils = context.JK.ProfileUtils
@ -31,7 +31,7 @@ profileUtils = context.JK.ProfileUtils
onAppInit: (@app) ->
@app.bindScreen('account/retailer', {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide})
onSchoolChanged: (retailerState) ->
onRetailerChanged: (retailerState) ->
@setState(retailerState)
onUserChanged: (userState) ->
@ -57,6 +57,7 @@ profileUtils = context.JK.ProfileUtils
beforeHide: (e) ->
#ProfileActions.viewTeacherProfileDone()
@screenVisible = false
return true
beforeShow: (e) ->
@ -141,7 +142,7 @@ profileUtils = context.JK.ProfileUtils
@app.ajaxError(jqXHR, null, null)
inviteTeacher: () ->
@app.layout.showDialog('invite-school-user', {d1: true})
@app.layout.showDialog('invite-retailer-user', {d1: true})
resendInvitation: (id, e) ->
e.preventDefault()
@ -169,13 +170,13 @@ profileUtils = context.JK.ProfileUtils
removeFromRetailer: (id, isTeacher, e) ->
if isTeacher
rest.deleteRetailerTeacher({id: this.state.retailer.id, teacher_id: id}).done((response) => @removeFromRetailerDone(response)).fail((jqXHR) => @removeFromSchoolFail(jqXHR))
rest.deleteRetailerTeacher({id: this.state.retailer.id, teacher_id: id}).done((response) => @removeFromRetailerDone(response)).fail((jqXHR) => @removeFromRetailerFail(jqXHR))
removeFromRetailerDone: (retailer) ->
context.JK.Banner.showNotice("User removed", "User was removed from your retailer.")
context.RetailerActions.updateRetailer(retailer)
removeFromSchoolFail: (jqXHR) ->
removeFromRetailerFail: (jqXHR) ->
@app.ajaxError(jqXHR)
renderUser: (user, isTeacher) ->
@ -217,6 +218,7 @@ profileUtils = context.JK.ProfileUtils
if this.state.retailer.teachers? && this.state.retailer.teachers.length > 0
for teacher in this.state.retailer.teachers
if teacher.user
teachers.push(@renderUser(teacher.user, true))
else
teachers = `<p>No teachers</p>`
@ -248,61 +250,30 @@ profileUtils = context.JK.ProfileUtils
@account()
account: () ->
ownerEmail = this.state.school.owner.email
correspondenceEmail = this.state.school.correspondence_email
correspondenceDisabled = !@isSchoolManaged()
nameErrors = context.JK.reactSingleFieldErrors('name', @state.updateErrors)
correspondenceEmailErrors = context.JK.reactSingleFieldErrors('correspondence_email', @state.updateErrors)
nameClasses = classNames({name: true, error: nameErrors?, field: true})
correspondenceEmailClasses = classNames({
correspondence_email: true,
error: correspondenceEmailErrors?,
field: true
})
cancelClasses = { "button-grey": true, "cancel" : true, disabled: this.state.updating }
updateClasses = { "button-orange": true, "update" : true, disabled: this.state.updating }
`<div className="account-block info-block">
<div className={nameClasses}>
<label>School Name:</label>
<label>Retailer Name:</label>
<input type="text" name="name" value={this.nameValue()} onChange={this.nameChanged}/>
{nameErrors}
</div>
<div className="field logo">
<label>School Logo:</label>
<AvatarEditLink target={this.state.school} target_type="school"/>
<label>Retailer Logo:</label>
<AvatarEditLink target={this.state.retailer} target_type="retailer"/>
</div>
<h4>Management Preference</h4>
<div className="field scheduling_communication">
<div className="scheduling_communication school">
<input type="radio" name="scheduling_communication" readOnly={true} value="school"
checked={this.isSchoolManaged()}/><label>School owner will manage scheduling of student lessons sourced
by JamKazam</label>
</div>
<div className="scheduling_communication teacher">
<input type="radio" name="scheduling_communication" readOnly={true} value="teacher"
checked={!this.isSchoolManaged()}/><label>Teacher will manage scheduling of lessons</label>
</div>
</div>
<div className={correspondenceEmailClasses}>
<label>Correspondence Email:</label>
<input type="text" name="correspondence_email" placeholder={ownerEmail} defaultValue={correspondenceEmail}
disabled={correspondenceDisabled}/>
<div className="hint">All emails relating to lesson scheduling will go to this email if school owner manages
scheduling.
</div>
{correspondenceEmailErrors}
</div>
<h4>Payments</h4>
<div className="field stripe-connect">
<StripeConnect purpose='school' user={this.state.user}/>
<StripeConnect purpose='retailer' user={this.state.user}/>
</div>
<div className="actions">
@ -340,7 +311,7 @@ profileUtils = context.JK.ProfileUtils
agreement: () ->
`<div className="agreement-block info-block">
<p>The agreement between your music school and JamKazam is part of JamKazam's terms of service. You can find the
<p>The agreement between your retailer and JamKazam is part of JamKazam's terms of service. You can find the
complete terms of service <a href="/corp/terms" target="_blank">here</a>. And you can find the section that is
most specific to the retailer terms <a href="/corp/terms" target="_blank">here</a>.</p>
</div>`

View File

@ -31,18 +31,28 @@ AvatarStore = context.AvatarStore
render: () ->
if this.props.target?.photo_url?
testStudentUrl = "/school/#{this.props.target.id}/student?preview=true"
testTeacherUrl = "/school/#{this.props.target.id}/teacher?preview=true"
target_type = this.props.target_type
testStudentUrl = "/#{target_type}/#{this.props.target.id}/student?preview=true"
testTeacherUrl = "/#{target_type}/#{this.props.target.id}/teacher?preview=true"
if target_type == 'school'
previewArea = `<div className="hint">See how it will look to&nbsp;
<a href={testStudentUrl} target="_blank">students</a> and&nbsp;
<a href={testTeacherUrl} target="_blank">teachers</a>
</div>`
else
previewArea = `<div className="hint">See how it will look to&nbsp;
<a href={testTeacherUrl} target="_blank">teachers</a>
</div>`
`<div className="avatar-edit-link">
<img src={this.props.target.photo_url}></img>
<br/>
<a onClick={this.startUpdate}>change/update logo</a><br/>
<div className="hint">See how it will look to&nbsp;
<a href={testStudentUrl} target="_blank">students</a> and&nbsp;
<a href={testTeacherUrl} target="_blank">teachers</a>
</div>
{previewArea}
</div>`
else
`<div className="avatar-edit-link">

View File

@ -0,0 +1,147 @@
context = window
RetailerStore = context.RetailerStore
@InviteRetailerUserDialog = React.createClass({
mixins: [Reflux.listenTo(@AppStore, "onAppInit"), Reflux.listenTo(RetailerStore, "onRetailerChanged")]
teacher: false
beforeShow: (args) ->
logger.debug("InviteRetailerUserDialog.beforeShow", args.d1)
@firstName = ''
@lastName = ''
@email = ''
@setState({inviteErrors: null, teacher: args.d1})
afterHide: () ->
onRetailerChanged: (retailerState) ->
@setState(retailerState)
onAppInit: (@app) ->
dialogBindings = {
'beforeShow': @beforeShow,
'afterHide': @afterHide
};
@app.bindDialog('invite-retailer-user', dialogBindings);
componentDidMount: () ->
@root = $(@getDOMNode())
getInitialState: () ->
{inviteErrors: null, retailer: null, sending: false}
doCancel: (e) ->
e.preventDefault()
@app.layout.closeDialog('invite-retailer-user', true);
doInvite: (e) ->
e.preventDefault()
if this.state.sending
console.log("sending already")
return
email = @root.find('input[name="email"]').val()
lastName = @root.find('input[name="last_name"]').val()
firstName = @root.find('input[name="first_name"]').val()
retailer = context.RetailerStore.getState().retailer
@setState({inviteErrors: null, sending: true})
rest.createRetailerInvitation({
id: retailer.id,
as_teacher: this.state.teacher,
email: email,
last_name: lastName,
first_name: firstName
}).done((response) => @createDone(response)).fail((jqXHR) => @createFail(jqXHR))
createDone: (response) ->
console.log("invitation added", response)
@setState({inviteErrors:null, sending: false})
context.RetailerActions.addInvitation(this.state.teacher, response)
context.JK.Banner.showNotice("invitation sent", "Your invitation has been sent!")
@app.layout.closeDialog('invite-retailer-user')
createFail: (jqXHR) ->
handled = false
if jqXHR.status == 422
errors = JSON.parse(jqXHR.responseText)
@setState({inviteErrors: errors, sending: false})
handled = true
if !handled
@app.ajaxError(jqXHR, null, null)
close: (e) ->
e.preventDefault()
@app.layout.closeDialog('invite-retailer-user');
renderRetailer: () ->
firstNameErrors = context.JK.reactSingleFieldErrors('first_name', @state.inviteErrors)
lastNameErrors = context.JK.reactSingleFieldErrors('last_name', @state.inviteErrors)
emailErrors = context.JK.reactSingleFieldErrors('email', @state.inviteErrors)
firstNameClasses = classNames({first_name: true, error: firstNameErrors?, field: true})
lastNameClasses = classNames({last_name: true, error: lastNameErrors?, field: true})
emailClasses = classNames({email: true, error: emailErrors?, field: true})
sendInvitationClasses = classNames({'button-orange': true, disabled: this.state.sending})
if @state.teacher
title = 'invite teacher'
help = `<p>Send invitations to teachers who teach through your music store. When your teachers accept this invitation to create teacher accounts on JamKazam, you can easily send emails to customers who purchase online lessons pointing these customers to your preferred teachers from your store. </p>`
else
title = 'invite student'
help = `<p>
Shouldn't be here...
</p>`
`<div>
<div className="content-head">
<img className="content-icon" src="/assets/content/icon_add.png" height={19} width={19}/>
<h1>{title}</h1>
</div>
<div className="dialog-inner">
{help}
<div className={firstNameClasses}>
<label>First Name: </label>
<input type="text" defaultValue={this.firstName} name="first_name"/>
{firstNameErrors}
</div>
<div className={lastNameClasses}>
<label>Last Name: </label>
<input type="text" defaultValue={this.lastName} name="last_name"/>
{lastNameErrors}
</div>
<div className={emailClasses}>
<label>Email Name: </label>
<input type="text" defaultValue={this.email} name="email"/>
{emailErrors}
</div>
<div className="actions">
<a onClick={this.doCancel} className="button-grey">CANCEL</a>
<a onClick={this.doInvite} className={sendInvitationClasses}>SEND INVITATION</a>
</div>
</div>
</div>`
render: () ->
retailer = this.state.retailer
if !retailer?
return `<div>no retailer</div>`
@renderRetailer()
})

View File

@ -30,7 +30,7 @@ SchoolStore = context.SchoolStore
@root = $(@getDOMNode())
getInitialState: () ->
{inviteErrors: null, school: null}
{inviteErrors: null, school: null, sending: false}
doCancel: (e) ->
e.preventDefault()
@ -39,11 +39,15 @@ SchoolStore = context.SchoolStore
doInvite: (e) ->
e.preventDefault()
if this.state.sending
console.log("sending already")
return
email = @root.find('input[name="email"]').val()
lastName = @root.find('input[name="last_name"]').val()
firstName = @root.find('input[name="first_name"]').val()
school = context.SchoolStore.getState().school
@setState({inviteErrors: null})
@setState({inviteErrors: null, sending: true})
rest.createSchoolInvitation({
id: school.id,
as_teacher: this.state.teacher,
@ -54,6 +58,7 @@ SchoolStore = context.SchoolStore
createDone: (response) ->
console.log("invitation added", response)
@setState({inviteErrors:null, sending: false})
context.SchoolActions.addInvitation(this.state.teacher, response)
context.JK.Banner.showNotice("invitation sent", "Your invitation has been sent!")
@app.layout.closeDialog('invite-school-user')
@ -63,7 +68,7 @@ SchoolStore = context.SchoolStore
if jqXHR.status == 422
errors = JSON.parse(jqXHR.responseText)
@setState({inviteErrors: errors})
@setState({inviteErrors: errors, sending: false})
handled = true
if !handled
@ -120,6 +125,7 @@ I'm writing to make you aware of a very interesting new option for private music
firstNameClasses = classNames({first_name: true, error: firstNameErrors?, field: true})
lastNameClasses = classNames({last_name: true, error: lastNameErrors?, field: true})
emailClasses = classNames({email: true, error: emailErrors?, field: true})
sendInvitationClasses = classNames({'button-orange': true, disabled: this.state.sending})
if @state.teacher
title = 'invite teacher'
@ -167,7 +173,7 @@ I'm writing to make you aware of a very interesting new option for private music
<div className="actions">
<a onClick={this.doCancel} className="button-grey">CANCEL</a>
<a onClick={this.doInvite} className="button-orange">SEND INVITATION</a>
<a onClick={this.doInvite} className={sendInvitationClasses}>SEND INVITATION</a>
</div>
</div>
</div>`

View File

@ -41,8 +41,12 @@ rest = new context.JK.Rest()
onPick: () ->
rest.generateSchoolFilePickerPolicy({id: @target.id})
.done((filepickerPolicy) =>
if @type == 'school'
genpolicy = rest.generateSchoolFilePickerPolicy({id: @target.id})
else if @type == 'retailer'
genpolicy = rest.generateRetailerFilePickerPolicy({id: @target.id})
genpolicy.done((filepickerPolicy) =>
@pickerOpen = true
@changed()
window.filepicker.setKey(gon.fp_apikey);
@ -69,7 +73,7 @@ rest = new context.JK.Rest()
.fail(@app.ajaxError)
afterImageUpload: (fpfile) ->
logger.debug("afterImageUploaded")
logger.debug("afterImageUploaded", typeof fpfile, fpfile)
$.cookie('original_fpfile', JSON.stringify(fpfile));
@currentFpfile = fpfile
@ -79,8 +83,12 @@ rest = new context.JK.Rest()
@signFpfile()
signFpfile: () ->
rest.generateSchoolFilePickerPolicy({ id: @target.id})
.done((policy) => (
if @type == 'school'
genpolicy = rest.generateSchoolFilePickerPolicy({id: @target.id})
else if @type == 'retailer'
genpolicy = rest.generateRetailerFilePickerPolicy({id: @target.id})
genpolicy.done((policy) => (
@signedCurrentFpfile = @currentFpfile.url + '?signature=' + policy.signature + '&policy=' + policy.policy;
@changed()
))
@ -125,6 +133,8 @@ rest = new context.JK.Rest()
if @type == 'school'
window.SchoolActions.refresh()
if @type == 'retailer'
window.RetailerActions.refresh()
@app.layout.closeDialog('upload-avatar')
@ -184,7 +194,10 @@ rest = new context.JK.Rest()
@updatingAvatar = true
@changed()
if @type == 'school'
rest.deleteSchoolAvatar({id: @target.id}).done((response) => @deleteDone(response)).fail((jqXHR) => @deleteFail(jqXHR))
else if @type == 'retailer'
rest.deleteRetailerAvatar({id: @target.id}).done((response) => @deleteDone(response)).fail((jqXHR) => @deleteFail(jqXHR))
deleteDone: (response) ->
@currentFpfile = null
@ -194,6 +207,8 @@ rest = new context.JK.Rest()
@currentCropSelection = null
if @type == 'school'
window.SchoolActions.refresh()
else if @type == 'retailer'
window.RetailerActions.refresh()
@app.layout.closeDialog('upload-avatar');
@ -219,8 +234,12 @@ rest = new context.JK.Rest()
logger.debug("Converting...");
fpfile = @determineCurrentFpfile();
rest.generateSchoolFilePickerPolicy({ id: @target.id, handle: fpfile.url, convert: true })
.done((filepickerPolicy) =>
if @type == 'school'
genpolicy = rest.generateSchoolFilePickerPolicy({ id: @target.id, handle: fpfile.url, convert: true })
else if @type == 'retailer'
genpolicy = rest.generateRetailerFilePickerPolicy({ id: @target.id, handle: fpfile.url, convert: true })
genpolicy.done((filepickerPolicy) =>
window.filepicker.setKey(gon.fp_apikey)
window.filepicker.convert(fpfile, {
crop: [
@ -243,8 +262,12 @@ rest = new context.JK.Rest()
scale: (cropped) ->
logger.debug("converting cropped");
rest.generateSchoolFilePickerPolicy({id: @target.id, handle: cropped.url, convert: true})
.done((filepickerPolicy) => (
if @type == 'school'
genpolicy = rest.generateSchoolFilePickerPolicy({id: @target.id, handle: cropped.url, convert: true})
else if @type == 'retailer'
genpolicy = rest.generateRetailerFilePickerPolicy({id: @target.id, handle: cropped.url, convert: true})
genpolicy.done((filepickerPolicy) => (
window.filepicker.convert(cropped, {
height: @targetCropSize,
width: @targetCropSize,
@ -275,14 +298,24 @@ rest = new context.JK.Rest()
updateServer: (scaledLarger, scaled, cropped) ->
logger.debug("converted and scaled final image %o", scaled);
rest.updateSchoolAvatar({
if @type == 'school'
update = rest.updateSchoolAvatar({
id: @target.id,
original_fpfile: @determineCurrentFpfile(),
cropped_fpfile: scaled,
cropped_large_fpfile: scaledLarger,
crop_selection: @selection
})
.done((response) => @updateAvatarSuccess(response))
else if @type == 'retailer'
update = rest.updateRetailerAvatar({
id: @target.id,
original_fpfile: @determineCurrentFpfile(),
cropped_fpfile: scaled,
cropped_large_fpfile: scaledLarger,
crop_selection: @selection
})
update.done((response) => @updateAvatarSuccess(response))
.fail(@app.ajaxError)
.always(() => (
@updatingAvatar = false

View File

@ -31,7 +31,7 @@ rest = new context.JK.Rest()
@teacherInvitations = response.entries
@changed()
onAddInvitation: (invitation) ->
onAddInvitation: (teacher, invitation) ->
@teacherInvitations.push(invitation)
@changed()

View File

@ -18,6 +18,8 @@ rest = new context.JK.Rest()
redirect = '/client#/account/school'
else if purpose == 'jamclass-home'
redirect = '/client#/jamclass'
else if purpose == 'retailer'
redirect = '/client#/account/retailer'
else
throw "unknown purpose #{purpose}"

View File

@ -48,4 +48,39 @@
margin-right: 3px;
}
}
.acct-prf-inst-ck {
.icheckbox_minimal {
margin-right: 8px;
position: relative;
top: 3px;
}
}
.acct-prf-inst-drdwn {
select.proficiency_selector {
color:black;
}
}
.user-genre-desc {
margin: 4px;
padding: 4px;
.icheckbox_minimal {
top: 4px;
position: relative;
}
span {
vertical-align: middle;
margin: 4px;
padding: 4px;
}
}
.instrument_selector {
}
}

View File

@ -0,0 +1,265 @@
@import "client/common";
#account-retailer {
div[data-react-class="AccountRetailerScreen"] {
height: 100%;
}
.profile-header {
padding: 10px 30px !important;
}
label {
display: inline-block;
min-width: 200px;
}
input {
min-width:200px;
}
.hint {
margin-left: 200px;
font-size: 12px;
font-style: italic;
margin-top: 5px;
}
.iradio_minimal {
display: inline-block;
top: 4px;
margin-right: 5px;
}
.field {
margin-bottom: 30px;
&.stripe-connect {
margin-bottom: 10px;
label {
margin-bottom: 10px;
}
}
}
.store-header {
float: left;
padding-top: 10px;
font-size: 20px;
font-weight: bold;
}
.profile-nav a {
position: absolute;
text-align: center;
height: 100%;
width: 98%;
margin: 0 auto;
padding: 11px 0 0 0;
@include border-box_sizing;
}
.profile-tile {
width: 25%;
float: left;
@include border-box_sizing;
height: 40px;
position: relative;
}
.profile-body {
padding-top: 100px;
}
.profile-photo {
width: 16%;
@include border-box_sizing;
}
.profile-nav {
margin: 0;
width: 84%;
}
.profile-wrapper {
padding: 10px 20px
}
.main-content {
float: left;
@include border-box_sizing;
width: 84%;
}
.info-block {
min-height:400px;
h3 {
font-weight: bold;
font-size: 18px;
margin-bottom: 10px;
}
h4 {
margin-bottom: 10px;
}
.section {
margin-bottom: 40px;
&.teachers {
clear: both;
}
}
table.jamtable {
font-size: 12px;
width: 100%;
}
}
.stripe-connect {
padding: 0;
border: 0;
background: transparent;
outline:transparent;
cursor:pointer;
}
.actions {
float: left;
margin-top: 30px;
margin-bottom: 10px;
}
a.cancel {
margin-left:3px;
}
.avatar-edit-link {
display:inline-block;
img {
max-width:200px;
}
}
.avatar-edit-link {
.hint {
margin-left:0;
}
}
.column {
width:50%;
@include border_box_sizing;
h3 {
float:left;
}
.invite-dialog {
float:right;
margin-right:2px;
}
&.column-left {
float:left;
padding-right:30px;
}
&.column-right {
float:right;
padding-left:30px;
}
.username {
max-width:40%;
font-size:16px;
color:white;
}
table {
width:100%;
}
td.description {
font-size:16px;
color: white;
vertical-align: top;
white-space: nowrap;
}
td.message {
color: $ColorTextTypical;
padding-left: 10px;
vertical-align: top;
text-align:right;
}
.detail-block {
display:inline-block;
font-size:12px;
}
.resend {
float:left;
}
.delete {
float:right;
}
.teacher-invites, .student-invites {
margin-bottom: 20px;
margin-top:40px;
font-size:12px;
min-height:40px;
p {
font-size:12px;
}
}
.teachers, .students {
margin-bottom:20px;
}
p {
font-size:12px;
margin-left:0;
}
.retailer-invitation {
margin-bottom:20px;
}
}
.retailer-user {
margin-bottom:20px;
.avatar {
position:absolute;
padding:1px;
width:32px;
height:32px;
background-color:#ed4818;
margin:0;
-webkit-border-radius:16px;
-moz-border-radius:16px;
border-radius:16px;
float:none;
}
.avatar img {
width: 32px;
height: 32px;
-webkit-border-radius:16px;
-moz-border-radius:16px;
border-radius:16px;
}
.usersname {
margin-left:56px;
line-height:32px;
vertical-align: middle;
display: inline-block;
}
.just-name {
display:block;
}
.just-email {
position: relative;
top: -14px;
font-size:12px;
}
.user-actions {
float: right;
line-height: 32px;
height: 32px;
vertical-align: middle;
font-size:12px;
}
}
p {
font-size:12px;
margin:0;
}
}

View File

@ -0,0 +1,39 @@
@import "client/common";
#invite-retailer-user-dialog {
width: 500px;
h3 {
color:white;
margin-bottom:20px;
}
.dialog-inner {
width: auto;
}
.actions {
clear: both;
text-align: center;
}
p { margin-bottom:20px;}
label {
width:150px;
display:inline-block;
}
input {
display:inline-block;
width:250px;
}
.field {
margin-bottom:20px;
}
textarea {
height:500px;
width:100%;
margin-bottom:20px;
}
}

View File

@ -115,6 +115,8 @@ class ApiUsersController < ApiController
teacher: params[:teacher],
school_invitation_code: params[:school_invitation_code],
school_id: params[:school_id],
retailer_invitation_code: params[:retailer_invitation_code],
retailer_id: params[:retailer_id],
school_interest: params[:school_interest],
education_interest: params[:education_interest],
affiliate_referral_id: cookies[:affiliate_visitor],

View File

@ -194,8 +194,25 @@
<div class="right">
<a id="account-school-link" href="#" class="button-orange">UPDATE</a>
</div>
{% } %}
<br clear="all" />
{% } %}
{% if (data.owned_retailer_id) { %}
<hr />
<div class="account-left">
<h2>retailer:</h2>
</div>
<div class="account-mid school">
<div class="whitespace">
<span class="retailer-info">Invite teachers, students, and manage your retailer settings.</span>
</div>
</div>
<div class="right">
<a id="account-retailer-link" href="#" class="button-orange">UPDATE</a>
</div>
<br clear="all" />
{% } %}
</div>
<!-- end content wrapper -->

View File

@ -84,8 +84,8 @@
<script type="text/template" id="account-profile-instrument">
<tr data-instrument-id='{id}'>
<td><input type="checkbox" {checked} />{description}</td>
<td align="right" width="50%">
<td class="acct-prf-inst-ck"><input type="checkbox" {checked} />{description}</td>
<td align="right" width="50%" class="acct-prf-inst-drdwn">
<select name="proficiency" class='proficiency_selector'>
<option value="1">Beginner</option>
<option value="2">Intermediate</option>
@ -96,5 +96,5 @@
</script>
<script type="text/template" id="template-user-setup-genres">
<tr><td><input value="{id}" {checked} type="checkbox" />{description}</td></tr>
<tr><td class="user-genre-desc"><input value="{id}" {checked} type="checkbox" /><span>{description}</span></td></tr>
</script>

View File

@ -50,6 +50,7 @@
= render 'dialogs/tryTestDriveDialog'
= render 'dialogs/uploadAvatarDialog'
= render 'dialogs/inviteSchoolUserDialog'
= render 'dialogs/inviteRetailerUserDialog'
= render 'dialogs/chatDialog'
= render 'dialogs/cancelLessonDialog'
= render 'dialogs/rescheduleLessonDialog'

View File

@ -0,0 +1,2 @@
.dialog.dialog-overlay-sm.top-parent layout='dialog' layout-id='invite-retailer-user' id='invite-retailer-user-dialog'
= react_component 'InviteRetailerUserDialog', {}

View File

@ -34,6 +34,8 @@ class UserManager < BaseManager
teacher = options[:teacher]
school_invitation_code = options[:school_invitation_code]
school_id = options[:school_id]
retailer_invitation_code = options[:retailer_invitation_code]
retailer_id = options[:retailer_id]
school_interest = options[:school_interest]
education_interest = options[:education_interest]
origin = options[:origin]
@ -87,6 +89,8 @@ class UserManager < BaseManager
teacher: teacher,
school_invitation_code: school_invitation_code,
school_id: school_id,
retailer_invitation_code: retailer_invitation_code,
retailer_id: retailer_id,
school_interest: school_interest,
education_interest: education_interest,
origin: origin,