496 lines
21 KiB
CoffeeScript
496 lines
21 KiB
CoffeeScript
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", "<p>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 <a href=\"/corp/terms\" target=\"_blank\">terms of service</a> 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.</p>")
|
||
else
|
||
context.JK.Banner.showAlert("Policy Issue", "<p>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 <a href=\"/corp/terms\" target=\"_blank\">terms of service</a>. You may reschedule this recurring lesson anytime starting from the end of this next scheduled lesson, so please plan to do it then.</p>")
|
||
else
|
||
if @viewerStudent()
|
||
context.JK.Banner.showAlert("Policy Issue", "<p>We’re sorry, but you cannot reschedule a lesson less than 24 hours before the lesson start time. This is not allowed in the <a href=\"/corp/terms\" target=\"_blank\">terms of service</a> because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost.</p>")
|
||
else
|
||
context.JK.Banner.showAlert("Policy Issue", "<p>We’re sorry, but you cannot reschedule a lesson less than 24 hours before the lesson start time. This is not allowed in the <a href=\"/corp/terms\" target=\"_blank\">terms of service</a>.</p>")
|
||
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 <a href=\"/corp/terms\" target=\"_blank\">terms of service</a> 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", "<p>We’re sorry, but you cannot cancel a lesson less than 24 hours before the lesson start time. This is not allowed in the <a href=\"/corp/terms\" target=\"_blank\">terms of service</a> because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost.</p>")
|
||
else
|
||
if @viewerStudent()
|
||
context.JK.Banner.showAlert("Policy Issue", "<p>We’re sorry, but you cannot cancel a lesson less than 24 hours before the lesson start time. This is not allowed in the <a href=\"/corp/terms\" target=\"_blank\">terms of service</a> because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost.</p>")
|
||
else
|
||
# path should not be taken
|
||
context.JK.Banner.showAlert("Policy Issue", "<p>We’re sorry, but you cannot cancel a lesson less than 24 hours before the lesson start time. This is not allowed in the <a href=\"/corp/terms\" target=\"_blank\">terms of service</a>.</p>")
|
||
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(`<a key={data.name} className="missing-profile" onClick={this.missingProfileClick.bind(this, data)}>{data.name}</a>`)
|
||
|
||
for name, data of matches
|
||
if data.text?
|
||
links.push(`<a key={data.name} onClick={this.onProfileHelp.bind(this, data)} className="missing-profile">{data.name}</a>`)
|
||
`<ul className="missing-profile-list">{links}</ul>`
|
||
|
||
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 `<div>Loading</div>`
|
||
|
||
if @viewerStudent()
|
||
searchTeachers = `<div className="search-teachers">
|
||
<h2>search teachers</h2>
|
||
|
||
<p>JamClass instructors are each individually screened to ensure that they are highly qualified music
|
||
teachers,
|
||
equipped to teach effectively online, and background checked.
|
||
</p>
|
||
|
||
<div className="actions">
|
||
<a href="/client#/teachers/search" className="button-orange">SEARCH TEACHERS</a>
|
||
</div>
|
||
</div>`
|
||
learnMoreLink = `<a href="/landing/jamclass/students" className="button-orange" target="_blank">LEARN MORE</a>`
|
||
learnMoreAboutJamClass = ` <p>
|
||
JamClass is the best way to take music lessons, offering significant advantages over both traditional
|
||
face-to-face lessons
|
||
and online skype lessons.
|
||
</p>`
|
||
signupTestDrive = `<div className="jamclass-section">
|
||
<h2>sign up for testdrive</h2>
|
||
|
||
<p>
|
||
There are two awesome, painless ways to get started with JamClass.
|
||
</p>
|
||
|
||
<p>
|
||
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!
|
||
</p>
|
||
|
||
<p>
|
||
Or take one JamClass lesson free. It's on us! We're confident you'll take more.
|
||
</p>
|
||
|
||
<p>
|
||
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.
|
||
</p>
|
||
|
||
<div className="actions">
|
||
<a href="/landing/jamclass/students" className="button-orange">SIGN UP FOR TESTDRIVE</a>
|
||
or
|
||
<a href="/client#/teachers/search" className="button-orange search-teachers-btn">SEARCH TEACHERS</a>
|
||
</div>
|
||
</div>`
|
||
else
|
||
searchTeachers = `<div className="search-teachers">
|
||
<h2>stripe status</h2>
|
||
<div className="field stripe-connect">
|
||
<StripeConnect purpose='jamclass-home' user={this.state.user}/>
|
||
</div>
|
||
</div>`
|
||
learnMoreLink = `<a href="/landing/jamclass/teachers" className="button-orange" target="_blank">LEARN MORE</a>`
|
||
learnMoreAboutJamClass = ` <p>
|
||
JamClass is the best way to teach music lessons, offering significant advantages over both traditional
|
||
face-to-face lessons
|
||
and online skype lessons.
|
||
</p>`
|
||
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 = `<p className="pct-complete">Your teacher profile is {pct}% complete.</p>`
|
||
else
|
||
pctCompleteMsg = `<p className="pct-complete">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:</p>`
|
||
missingLinks = @constructMissingLinks(this.state.user)
|
||
|
||
signupTestDrive = `<div className="jamclass-section">
|
||
{pctCompleteMsg}
|
||
{missingLinks}
|
||
<a className="button-orange view-teacher-profile" onClick={this.viewTeacherProfile}>VIEW TEACHER PROFILE</a>
|
||
</div>`
|
||
|
||
classes = []
|
||
if @state.updating
|
||
classes = [`<tr key="loading">
|
||
<td className="loading" colSpan="5">Loading...</td>
|
||
</tr>`]
|
||
else
|
||
for lessonData in @lessons()
|
||
|
||
if lessonData.hasUnreadMessages
|
||
unreadMessages = `<a onClick={this.onReadMessage.bind(this, lessonData)}><img src="/assets/content/icon_unread_mail.png"/></a>`
|
||
else
|
||
unreadMessages = null
|
||
lesson = `<tr key={lessonData.id} className="lesson-session" data-lesson-session-id={lessonData.id}>
|
||
<td className="userColumn"><div className="avatar"><img src={lessonData.other.resolved_photo_url} /></div><a href={lessonData.other.best_profile}><span className="first_name">{lessonData.other.first_name}</span><br/><span className="last_name">{lessonData.other.last_name}</span></a></td>
|
||
<td className="startTimeColumn">{lessonData.music_session.pretty_scheduled_start_with_timezone}</td>
|
||
<td className="displayStatusColumn">{lessonData.displayStatus}</td>
|
||
<td className="unreadColumn">{unreadMessages}</td>
|
||
<td className="actionsColumn"><a data-lesson-id={lessonData.id} className="lesson-session-actions-btn" onClick={this.openMenu.bind(this, lessonData)}>menu <div className="details-arrow arrow-down"/></a></td>
|
||
</tr>`
|
||
|
||
classes.push(lesson)
|
||
if classes.length == 0
|
||
classes = [`<tr key="none">
|
||
<td className="none" colSpan="5">No Lessons Yet</td>
|
||
</tr>`]
|
||
|
||
`<div className="content-body-scroller">
|
||
<div className="column column-left">
|
||
<h2>my lessons</h2>
|
||
<div className="my-lessons jamclass-section">
|
||
<table className="jamtable">
|
||
<thead>
|
||
<tr>
|
||
<th className="role userColumn">{this.otherRole()}</th>
|
||
<th className="startTimeColumn">DATE/TIME</th>
|
||
<th className="displayStatusColumn">STATUS</th>
|
||
<th className="unreadColumn"></th>
|
||
<th className="actionsColumn">ACTIONS</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{classes}
|
||
</tbody>
|
||
</table>
|
||
|
||
<div style={{display:'none'}} className="calender-integration-notice">
|
||
Don't miss a lesson! <a>Integrate your lessons into your calendar.</a>
|
||
</div>
|
||
</div>
|
||
{searchTeachers}
|
||
</div>
|
||
<div className="column column-right">
|
||
<div className="jamclass-section">
|
||
<h2>learn about jamclass</h2>
|
||
|
||
{learnMoreAboutJamClass}
|
||
|
||
<div className="actions">
|
||
{learnMoreLink}
|
||
</div>
|
||
</div>
|
||
{signupTestDrive}
|
||
<div className="jamclass-section">
|
||
<h2>get ready for your first lesson</h2>
|
||
|
||
<p>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 <a href="mailto:support@jamkazam.com">support@jamkazam.com</a> anytime, and
|
||
read our
|
||
<a onClick={alert.bind('not yet')}>JamClass user guide</a> to learn how to use all the lesson features.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<br className="clearall"/>
|
||
</div>`
|
||
|
||
|
||
viewerStudent: () ->
|
||
!@viewerTeacher()
|
||
|
||
viewerTeacher: () ->
|
||
this.state.user?.is_a_teacher
|
||
|
||
myRole: () ->
|
||
if @viewerStudent()
|
||
'student'
|
||
else
|
||
'teacher'
|
||
otherRole: () ->
|
||
if @viewerStudent()
|
||
'teacher'
|
||
else
|
||
'student'
|
||
}) |