555 lines
19 KiB
CoffeeScript
555 lines
19 KiB
CoffeeScript
context = window
|
|
rest = context.JK.Rest()
|
|
logger = context.JK.logger
|
|
|
|
UserStore = context.UserStore
|
|
|
|
@BookLesson = React.createClass({
|
|
|
|
mixins: [
|
|
ICheckMixin,
|
|
Reflux.listenTo(AppStore, "onAppInit"),
|
|
Reflux.listenTo(UserStore, "onUserChanged")
|
|
]
|
|
|
|
onAppInit: (@app) ->
|
|
@app.bindScreen('jamclass/book-lesson',
|
|
{beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide})
|
|
|
|
onUserChanged: (userState) ->
|
|
@setState({user: userState?.user})
|
|
|
|
checkboxChanged: (e) ->
|
|
checked = $(e.target).is(':checked')
|
|
|
|
value = $(e.target).val()
|
|
|
|
@setState({ recurring: value })
|
|
|
|
componentDidMount: () ->
|
|
@checkboxes = [{selector: 'input.lesson-frequency', stateKey: 'lesson-frequency'}]
|
|
@root = $(@getDOMNode())
|
|
|
|
@slot1Date = @root.find('.slot-1 .date-picker')
|
|
@slot2Date = @root.find('.slot-2 .date-picker')
|
|
@slot1Date.datepicker({
|
|
dateFormat: "D M d yy",
|
|
onSelect: ((e) => @toggleDate(e))
|
|
})
|
|
@slot2Date.datepicker({
|
|
dateFormat: "D M d yy",
|
|
onSelect: ((e) => @toggleDate(e))
|
|
})
|
|
@iCheckify()
|
|
|
|
componentDidUpdate:() ->
|
|
@iCheckify()
|
|
@slot1Date = @root.find('.slot-1 .date-picker')
|
|
@slot2Date = @root.find('.slot-2 .date-picker')
|
|
@slot1Date.datepicker({
|
|
dateFormat: "D M d yy",
|
|
onSelect: ((e) => @toggleDate(e))
|
|
})
|
|
@slot2Date.datepicker({
|
|
dateFormat: "D M d yy",
|
|
onSelect: ((e) => @toggleDate(e))
|
|
})
|
|
|
|
|
|
toggleDate: (e) ->
|
|
|
|
isNormal: () ->
|
|
@state.type == 'normal'
|
|
|
|
isTestDrive: () ->
|
|
@state.type?.indexOf('test-drive') == 0
|
|
|
|
parseId:(id) ->
|
|
if !id?
|
|
{id: null, type: null}
|
|
else
|
|
bits = id.split('_')
|
|
if bits.length == 2
|
|
{id: bits[1], type: bits[0]}
|
|
else
|
|
{id: null, type: null}
|
|
|
|
beforeHide: (e) ->
|
|
logger.debug("BookLesson: beforeHide")
|
|
@resetErrors()
|
|
|
|
beforeShow: (e) ->
|
|
|
|
afterShow: (e) ->
|
|
logger.debug("BookLesson: afterShow", e.id)
|
|
|
|
parsed = @parseId(e.id)
|
|
|
|
id = parsed.id
|
|
|
|
@setState({teacherId: id, type: parsed.type})
|
|
@resetErrors()
|
|
rest.getUserDetail({
|
|
id: id,
|
|
show_teacher: true
|
|
}).done((response) => @userDetailDone(response)).fail(@app.ajaxError)
|
|
|
|
userDetailDone: (response) ->
|
|
if response.id == @state.teacherId
|
|
@setState({teacher: response, isSelf: response.id == context.JK.currentUserId})
|
|
else
|
|
logger.debug("BookLesson: ignoring teacher details", response.id, @state.teacherId)
|
|
|
|
getInitialState: () ->
|
|
{
|
|
user: null,
|
|
teacher: null,
|
|
teacherId: null,
|
|
generalErrors: null,
|
|
descriptionErrors: null,
|
|
bookedPriceErrors: null,
|
|
slot1Errors: null,
|
|
slot2Errors: null
|
|
updating: false,
|
|
recurring: 'single'
|
|
}
|
|
|
|
jamclassPolicies: (e) ->
|
|
e.preventDefault()
|
|
context.JK.popExternalLink($(e.target).attr('href'))
|
|
|
|
getSlotData: (position) ->
|
|
$slot = @root.find('.slot-' + (position + 1))
|
|
picker = $slot.find('.date-picker')
|
|
|
|
hour = $slot.find('.hour').val()
|
|
minute = $slot.find('.minute').val()
|
|
am_pm = $slot.find('.am_pm').val()
|
|
|
|
|
|
if hour? and hour != ''
|
|
hour = new Number(hour)
|
|
if am_pm == 'PM'
|
|
hour += 12
|
|
else
|
|
hour = null
|
|
|
|
if minute? and minute != ''
|
|
minute = new Number(minute)
|
|
else
|
|
minute = null
|
|
|
|
if !@isRecurring()
|
|
date = picker.datepicker("getDate")
|
|
if date?
|
|
date = context.JK.formatDateYYYYMMDD(date)
|
|
else
|
|
day_of_week = $slot.find('.day_of_week').val()
|
|
|
|
|
|
{hour: hour, minute: minute, date: date, day_of_week: day_of_week}
|
|
|
|
resetErrors: () ->
|
|
@setState({generalErrors: null, slot1Errors: null, slot2Errors: null, descriptionErrors: null, bookedPriceErrors: null})
|
|
|
|
isRecurring: () ->
|
|
@state.recurring == 'recurring'
|
|
|
|
isMonthly: () ->
|
|
if !@isRecurring()
|
|
return false
|
|
|
|
parsed = @bookingOption()
|
|
return parsed? && parsed.frequency == 'monthly'
|
|
|
|
bookingOption: () ->
|
|
select = @root.find('.booking-options-for-teacher')
|
|
value = select.val()
|
|
@parseBookingOption(value)
|
|
|
|
# select format = frequency|lesson_length , where frequency is 'monthly' or 'weekly'
|
|
parseBookingOption: (value) ->
|
|
if !value?
|
|
return null
|
|
bits = value.split('|')
|
|
if !bits? || bits.length != 2
|
|
return null
|
|
return {frequency: bits[0], lesson_length: bits[1]}
|
|
|
|
|
|
onBookLesson: (e) ->
|
|
e.preventDefault()
|
|
|
|
logger.debug("user requested to book lesson")
|
|
|
|
if $(e.target).is('.disabled')
|
|
return
|
|
|
|
options = {}
|
|
options.teacher = this.state.teacher.id
|
|
|
|
options.slots = [@getSlotData(0), @getSlotData(1)]
|
|
options.timezone = window.jstz.determine().name()
|
|
description = @root.find('textarea.user-description').val()
|
|
if description == ''
|
|
description == null
|
|
options.description = description
|
|
|
|
if @isTestDrive()
|
|
options.payment_style = 'elsewhere'
|
|
options.lesson_type = 'test-drive'
|
|
|
|
else if @isNormal()
|
|
options.lesson_type = 'paid'
|
|
if @isRecurring()
|
|
if @isMonthly()
|
|
options.payment_style = 'monthly'
|
|
else
|
|
options.payment_style = 'weekly'
|
|
else
|
|
options.payment_style = 'single'
|
|
|
|
options.recurring = @isRecurring()
|
|
parsed = @bookingOption()
|
|
if parsed?
|
|
options.lesson_length = parsed.lesson_length
|
|
|
|
|
|
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))
|
|
|
|
booked: (response) ->
|
|
@setState({updating: false})
|
|
UserActions.refresh()
|
|
if response.user['has_stored_credit_card?']
|
|
context.JK.Banner.showNotice("Lesson Requested","The teacher has been notified of your lesson request, and should respond soon.<br/><br/>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.id}"
|
|
url = "/client#/jamclass"
|
|
context.location = url
|
|
else
|
|
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: {}}
|
|
for errorType, errors of body.errors
|
|
if errorType == 'description'
|
|
@setState({descriptionErrors: errors})
|
|
else if errorType == 'booked_price'
|
|
@setState({bookedPriceErrors: errors})
|
|
else if errorType == 'lesson_length'
|
|
# swallow, because 'booked_price' covers this
|
|
else if errorType == 'lesson_booking_slots'
|
|
# do nothing. these are handled better by the _children errors
|
|
else
|
|
generalErrors.errors[errorType] = errors
|
|
|
|
for childErrorType, childErrors of body._children
|
|
if childErrorType == 'lesson_booking_slots'
|
|
slot1Errors = childErrors[0]
|
|
slot2Errors = childErrors[1]
|
|
if Object.keys(slot1Errors["errors"]).length > 0
|
|
@setState({slot1Errors: slot1Errors})
|
|
if Object.keys(slot2Errors["errors"]).length > 0
|
|
@setState({slot2Errors: slot2Errors})
|
|
if Object.keys(generalErrors.errors).length > 0
|
|
@setState({generalErrors: generalErrors})
|
|
|
|
onCancel: (e) ->
|
|
e.preventDefault()
|
|
|
|
isNormal: () ->
|
|
@state.type == 'normal'
|
|
|
|
constructBookingOptions: () ->
|
|
results = []
|
|
if !@state.teacher?
|
|
return results
|
|
|
|
teacher = @state.teacher.teacher
|
|
enabledMinutes = []
|
|
for minutes in [30, 45, 60, 90, 120]
|
|
duration_enabled = teacher["lesson_duration_#{minutes}"]
|
|
|
|
if duration_enabled
|
|
enabledMinutes.push(minutes)
|
|
|
|
if !@isRecurring()
|
|
for minutes in enabledMinutes
|
|
lesson_price = teacher["price_per_lesson_#{minutes}_cents"]
|
|
value = "single|#{minutes}"
|
|
display = "#{minutes} Minute Lesson for $#{(lesson_price / 100).toFixed(2)}"
|
|
results.push(`<option value={value}>{display}</option>`)
|
|
else
|
|
for minutes in enabledMinutes
|
|
lesson_price = teacher["price_per_lesson_#{minutes}_cents"]
|
|
value = "single|#{minutes}"
|
|
display = "#{minutes} Minute Lesson Each Week - $#{(lesson_price / 100).toFixed(2)} Per Week"
|
|
results.push(`<option value={value}>{display}</option>`)
|
|
|
|
|
|
for minutes in enabledMinutes
|
|
monthly_price = teacher["price_per_month_#{minutes}_cents"]
|
|
value = "monthly|#{minutes}"
|
|
display = "#{minutes} Minute Lesson Each Week - $#{(monthly_price / 100).toFixed(2)} Per Month"
|
|
results.push(`<option value={value}>{display}</option>`)
|
|
|
|
if results.length == 0
|
|
results.push(`<option value=''>This teacher has no pricing options</option>`)
|
|
else
|
|
results.unshift(`<option value=''>Please choose an option...</option>`)
|
|
results
|
|
|
|
render: () ->
|
|
teacher = @state.teacher
|
|
|
|
photo_url = teacher?.photo_url
|
|
if !photo_url?
|
|
photo_url = '/assets/shared/avatar_generic.png'
|
|
|
|
|
|
if teacher?
|
|
name = `<div className="teacher-name">{teacher.name}</div>`
|
|
teacher_first_name = teacher.first_name
|
|
else
|
|
name = `<div className="teacher-name">Loading...</div>`
|
|
teacher_first_name = '...'
|
|
|
|
hours = []
|
|
for hour in ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']
|
|
if hour == '12'
|
|
key = '00'
|
|
else
|
|
key = hour
|
|
hours.push(`<option key={key} value={key}>{hour}</option>`)
|
|
|
|
minutes = []
|
|
for minute in ['00', '15', '30', '45']
|
|
minutes.push(`<option key={minute} value={minute}>{minute}</option>`)
|
|
|
|
am_pm = [`<option key="AM" value="AM">AM</option>`, `<option key="PM" value="PM">PM</option>`]
|
|
|
|
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)
|
|
bookedPriceErrors = context.JK.reactSingleFieldErrors('booked_price', @state.bookedPriceErrors)
|
|
slot1Errors = context.JK.reactErrors(@state.slot1Errors, {preferred_day: 'Date', day_of_week: 'Day'})
|
|
slot2Errors = context.JK.reactErrors(@state.slot2Errors, {preferred_day: 'Date', day_of_week: 'Day'})
|
|
generalErrors = context.JK.reactErrors(@state.generalErrors, {user: 'You'})
|
|
|
|
bookedPriceClasses = classNames({booked_price: true, error: bookedPriceErrors?, field: true, 'booking-options': true})
|
|
descriptionClasses = classNames({description: true, error: descriptionErrors?})
|
|
slot1Classes = classNames({slot: true, 'slot-1': true, error: slot1Errors?})
|
|
slot2Classes = classNames({slot: true, 'slot-2': true, error: slot2Errors?})
|
|
generalClasses = classNames({general: true, error: generalErrors?})
|
|
|
|
|
|
if !@isRecurring()
|
|
slots =
|
|
`<div className="slots">
|
|
<div className={slot1Classes}>
|
|
<div className="slot-prompt">What date/time do you prefer for your lesson?</div>
|
|
<div className="field date">
|
|
<label>Date:</label>
|
|
|
|
<input className="date-picker" name="slot-1-date" type="text" data-slot="1"></input>
|
|
</div>
|
|
<div className="field time">
|
|
<label>Time:</label>
|
|
<select className="hour">{hours}</select> : <select className="minute">{minutes}</select> <select
|
|
className="am_pm">{am_pm}</select>
|
|
</div>
|
|
{slot1Errors}
|
|
</div>
|
|
<div className={slot2Classes}>
|
|
<div className="slot-prompt">What is a second date/time option if preferred not available?</div>
|
|
<div className="field date">
|
|
<label>Date:</label>
|
|
|
|
<input className="date-picker" name="slot-2-date" type="text" data-slot="2"></input>
|
|
</div>
|
|
<div className="field time">
|
|
<label>Time:</label>
|
|
<select className="hour">{hours}</select> : <select className="minute">{minutes}</select> <select
|
|
className="am_pm">{am_pm}</select>
|
|
</div>
|
|
{slot2Errors}
|
|
</div>
|
|
</div>`
|
|
else
|
|
days = []
|
|
days.push(`<option value=''>Choose a day of the week...</option>`)
|
|
days.push(`<option value="0">Sunday</option>`)
|
|
days.push(`<option value="1">Monday</option>`)
|
|
days.push(`<option value="2">Tuesday</option>`)
|
|
days.push(`<option value="3">Wednesday</option>`)
|
|
days.push(`<option value="4">Thursday</option>`)
|
|
days.push(`<option value="5">Friday</option>`)
|
|
days.push(`<option value="6">Saturday</option>`)
|
|
|
|
|
|
slots =
|
|
`<div className="slots">
|
|
<div className={slot1Classes}>
|
|
<div className="slot-prompt">What day/time do you prefer for your lesson?</div>
|
|
<div className="field date">
|
|
<label>Day:</label>
|
|
|
|
<select name="day-of-week-1" className="day_of_week" data-slot="1">{days}</select>
|
|
</div>
|
|
<div className="field time">
|
|
<label>Time:</label>
|
|
<select className="hour">{hours}</select> : <select className="minute">{minutes}</select> <select
|
|
className="am_pm">{am_pm}</select>
|
|
</div>
|
|
{slot1Errors}
|
|
</div>
|
|
<div className={slot2Classes}>
|
|
<div className="slot-prompt">What is a second day/time option if preferred not available?</div>
|
|
<div className="field date">
|
|
<label>Day:</label>
|
|
|
|
<select name="day-of-week-2" className="day_of_week" data-slot="2">{days}</select>
|
|
</div>
|
|
<div className="field time">
|
|
<label>Time:</label>
|
|
<select className="hour">{hours}</select> : <select className="minute">{minutes}</select> <select
|
|
className="am_pm">{am_pm}</select>
|
|
</div>
|
|
{slot2Errors}
|
|
</div>
|
|
</div>`
|
|
|
|
|
|
if @isTestDrive()
|
|
header = `<h2>book testdrive lesson</h2>`
|
|
if @state.user?.remaining_test_drives == 1
|
|
testDriveLessons = "1 TestDrive lesson credit"
|
|
else
|
|
testDriveLessons = "#{this.state.user.remaining_test_drives} TestDrive lesson credits"
|
|
|
|
actions = `<div className="actions left">
|
|
<a className={cancelClasses} onClick={this.onCancel}>CANCEL</a>
|
|
<a className={bookLessonClasses} onClick={this.onBookLesson}>BOOK TESTDRIVE LESSON</a>
|
|
</div>`
|
|
|
|
testDriveCredits = 1
|
|
|
|
if this.state.user.lesson_package_type_id == 'test-drive'
|
|
testDriveCredits = 4
|
|
else if this.state.user.lesson_package_type_id == 'test-drive-1'
|
|
testDriveCredits = 1
|
|
else if this.state.user.lesson_package_type_id == 'test-drive-2'
|
|
testDriveCredits = 2
|
|
|
|
if this.state.user.remaining_test_drives > 0
|
|
testDriveBookingInfo = `<div className="booking-info">
|
|
|
|
<p>You are booking a single 30-minute TestDrive session.</p>
|
|
|
|
<p>You currently have {testDriveLessons} available. If you need to cancel, you must cancel at least 24 hours before the lesson is scheduled to start, or you will be charged 1 TestDrive lesson credit.<br/>
|
|
|
|
<div className="jamclass-policies"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass
|
|
policies</a></div>
|
|
</p>
|
|
</div>`
|
|
else
|
|
testDriveBookingInfo = `<div className="booking-info">
|
|
|
|
<p>You are booking a single 30-minute TestDrive session.</p>
|
|
|
|
<p>Once payment is entered on the next screen, the teacher will be notified, and this lesson will then use 1 of {testDriveCredits} TestDrive credits. If you need to cancel, you must cancel at least 24 hours before the lesson is scheduled to start, or you will be charged 1 TestDrive lesson credit.<br/>
|
|
|
|
<div className="jamclass-policies"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass
|
|
policies</a></div>
|
|
</p>
|
|
</div>`
|
|
|
|
|
|
columnLeft = `<div className="column column-left">
|
|
{header}
|
|
{slots}
|
|
<div className={descriptionClasses}>
|
|
<div className="description-prompt">Tell {teacher_first_name} a little about yourself as a student.</div>
|
|
<textarea name="user-description" className="user-description" defaultValue=""/>
|
|
{descriptionErrors}
|
|
</div>
|
|
{actions}
|
|
<br className="clearall"/>
|
|
<div className={generalClasses}>
|
|
{generalErrors}
|
|
</div>
|
|
</div>`
|
|
|
|
columnRight = `<div className="column column-right">
|
|
<div className="teacher-header">
|
|
<div className="avatar">
|
|
<img src={photo_url}/>
|
|
</div>
|
|
{name}
|
|
</div>
|
|
{testDriveBookingInfo}
|
|
</div>`
|
|
|
|
else if @isNormal()
|
|
bookingOptionsForTeacher = @constructBookingOptions()
|
|
header = `<h2>book a lesson with {teacher_first_name}</h2>`
|
|
outActions = `<div className="actions right ">
|
|
<a className={cancelClasses} onClick={this.onCancel}>CANCEL</a>
|
|
<a className={bookLessonClasses} onClick={this.onBookLesson}>BOOK LESSON</a>
|
|
</div>`
|
|
|
|
columnLeft = `<div className="column column-left">
|
|
{header}
|
|
<div className="lesson-frequency">
|
|
<div className="field lesson-frequency lesson-frequency-single">
|
|
<input type="radio" name="lesson-frequency" value="single" checked={!this.isRecurring()}/><label htmlFor="lesson-frequency">A single lesson</label>
|
|
</div>
|
|
<div className="field lesson-frequency lesson-frequency-recurring">
|
|
<input type="radio" name="lesson-frequency" value="recurring" checked={this.isRecurring()}/><label htmlFor="lesson-frequency">A series of recurring weekly lessons</label>
|
|
</div>
|
|
</div>
|
|
<div className={bookedPriceClasses}>
|
|
<label>What lesson length and payment option do you prefer?</label>
|
|
<select name="booking-options-for-teacher" className="booking-options-for-teacher">{bookingOptionsForTeacher}</select>
|
|
{bookedPriceErrors}
|
|
</div>
|
|
<div className={descriptionClasses}>
|
|
<div className="description-prompt">Tell {teacher_first_name} a little about yourself as a student.</div>
|
|
<textarea name="user-description" className="user-description" defaultValue=""/>
|
|
{descriptionErrors}
|
|
</div>
|
|
</div>`
|
|
|
|
columnRight =
|
|
`<div className="column column-right">
|
|
{slots}
|
|
<br className="clearall"/>
|
|
<div className={generalClasses}>
|
|
{generalErrors}
|
|
</div>
|
|
</div>`
|
|
|
|
`<div className="content-body-scroller">
|
|
<Nav/>
|
|
<div className="lesson-booking">
|
|
{columnLeft}
|
|
{columnRight}
|
|
<br className="clearall"/>
|
|
{outActions}
|
|
<br className="clearall"/>
|
|
</div>
|
|
</div>`
|
|
|
|
|
|
}) |