Last active
March 3, 2020 15:20
-
-
Save lubieowoce/a1c454f20e089d9022761dfd511c4f67 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 2020.03.03-r3 | |
/* eslint-env jquery */ | |
/* eslint no-global-assign: "off" */ | |
/* eslint no-unused-vars: ["warn", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }] */ | |
/* eslint no-prototype-builtins: "off" */ | |
/* global | |
ich | |
calendar, venue | |
urlReverse | |
Message, messages | |
BaseReservationEvent, ReservationStub, ReservationEvent | |
ReservationDetailsPopover, cleanPopovers | |
*/ | |
/* global | |
old_ReservationDetailsPopover | |
_refreshPopovers | |
RegExp_escape | |
parse_annotation_data | |
get_annotation_data | |
get_annotation_text | |
modify_annotation_data | |
serialize_annotation_data | |
popover_annotation | |
modify_popover_annotation | |
get_event_details_uninited_annotation_tag | |
get_popover_annotation | |
serialize_popover_data | |
save_popover_data | |
modify_popover_annotation_data | |
write_popover_data | |
get_benefit_reservation | |
create_client | |
is_class_reservation | |
is_unavailability | |
is_single_reservation | |
count_cards | |
card_count_badge_state | |
card_count_badge_render | |
spinner_small | |
html_entities_decode | |
fetch_event_price_info | |
parse_class_reservation | |
class_event_reservations | |
class_event_reservations_raw | |
group_by | |
add_benefit_reservation | |
VENUE_PRICE_INFO | |
*/ | |
_refreshPopovers = () => { | |
cleanPopovers(); | |
(Object.values(calendar.reservationsById) | |
.filter((e)=>e.detailsPopover) | |
.forEach((e)=>{ | |
e.detailsPopover.outdate(); | |
e.detailsPopover.destroy(); | |
e.createDetailsPopover(); | |
}) | |
); | |
} | |
{ | |
RegExp_escape = (s) => { | |
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); // $& means the whole matched string | |
// return s.replace(/[.*+\-?\^${}()|[\]\\]/g, '\\$&'); | |
} | |
const SEPARATOR_RAW = '---' | |
const SEPARATOR = '\n'+SEPARATOR_RAW+'\n' | |
const SEPARATOR_REGEX = new RegExp('^'+RegExp_escape(SEPARATOR_RAW)+'$', 'm') // /m - each line separately | |
parse_annotation_data = (ann) => { | |
let val, ann_text | |
let match = ann.match(SEPARATOR_REGEX) | |
if (match === null) { | |
// console.log('parse_annotation_data ::', 'no separator found', ann) | |
ann_text = ann | |
val = null | |
} else { | |
// console.log('parse_annotation_data ::', 'found separator', match) | |
ann_text = ann.substring(0, match.index - 1) // before leading newline | |
let ann_val = ann.substring(match.index + match[0].length + 1) // after trailing newline | |
// console.log('parse_annotation_data ::', 'value', ann_val) | |
val = JSON.parse(ann_val) | |
} | |
return [ann_text, val] | |
} | |
get_annotation_text = (ann) => parse_annotation_data(ann)[0] | |
get_annotation_data = (ann) => parse_annotation_data(ann)[1] | |
serialize_annotation_data = (ann_text, data) => ( | |
ann_text + ((data === null) ? "" : (SEPARATOR + JSON.stringify(data))) | |
) | |
// modify_annotation_data = (ann, f) => { | |
// const INITIAL_STATE = {} | |
// let [ann_text, SEPARATOR, val] = parse_annotation_data(ann) | |
// let val2 = f((val === null) ? INITIAL_STATE : val) | |
// return [ann_text, SEPARATOR, JSON.stringify(val2)].join('') | |
// } | |
} | |
popover_annotation = (popover) => { | |
let event_details = popover.popoverContent[0] // .event-details | |
// let event_ann = event_details.querySelector('#popoverReservationAnnotation').children[0].querySelector('.smartform-enabled, .smartform-disabled, .smartform-static') | |
let ann_wrapper = event_details.querySelector('#popoverReservationAnnotation li:not(.custom-wrapper)') | |
return ann_wrapper.querySelector('.smartform-enabled, .smartform-disabled, .smartform-static') | |
} | |
modify_popover_annotation = (popover, f) => { | |
let ann = popover_annotation(popover) | |
// console.log('popover annotation', ann) | |
let $ann = $(ann) | |
let ann_sf = $ann.data('bs.smartform') | |
ann_sf.enable() | |
let val = ann_sf.inputElements().val() | |
ann_sf.inputElements().val(f(val)) | |
ann_sf.submit() | |
ann_sf.makestatic() | |
// ann_sf.cancel() | |
} | |
// get_popover_annotation = (popover) => { | |
// let ann = popover_annotation(popover) | |
// console.log('popover annotation', ann) | |
// let $ann = $(ann) | |
// if (ann.classList.contains('smartform-static')) { | |
// console.log('static') | |
// return $ann.find('.form-control-static[replaced="TEXTAREA"]')[0].innerText | |
// } else { | |
// console.log('active') | |
// return $ann.find('[name="annotation"]').val() | |
// } | |
// } | |
get_event_details_uninited_annotation_tag = (event_details) => { | |
return event_details.querySelector('[name="annotation"]') | |
} | |
get_popover_annotation = (popover) => { | |
let ann = popover_annotation(popover) | |
// console.log('popover annotation', ann) | |
let $ann = $(ann) | |
let ann_sf = $ann.data('bs.smartform') | |
ann_sf.enable() | |
let val = ann_sf.inputElements().val() | |
ann_sf.makestatic() | |
return val | |
} | |
write_popover_data = (popover, data) => { | |
let old_ann = get_popover_annotation(popover) | |
let ann_text = get_annotation_text(old_ann) | |
let new_ann = serialize_annotation_data(ann_text, data) | |
modify_popover_annotation(popover, (_)=>new_ann) | |
} | |
// serialize_popover_data = (popover) => { | |
// let $custom_input = $(popover.popoverContent[0]).find('.custom-wrapper') | |
// let data_serialized = $custom_input.attr('data-extracted') | |
// let data = JSON.parse(data_serialized) | |
// let [ann_text, sep, _] = parse_annotation_data(get_popover_annotation(popover)) | |
// return ann_text + ((data === null) ? "" : (sep + data_serialized)) | |
// } | |
// save_popover_data = (popover) => { | |
// let ann = serialize_popover_data(popover) | |
// modify_popover_annotation(popover, (_) => ann) | |
// } | |
// modify_popover_annotation_data = (popover, f) => { | |
// let $custom_input = $(popover.popoverContent[0]).find('.custom-wrapper') | |
// let data = JSON.parse($custom_input.attr('data-extracted')) || {users: []} | |
// let new_data = f(data) | |
// let new_data_serialized = JSON.stringify(new_data) | |
// $custom_input.attr('data-extracted', new_data_serialized) | |
// save_popover_data(popover) | |
// } | |
get_benefit_reservation = () => ( | |
Object.entries(calendar.reservationsById) | |
.filter(([_id, r]) => ( | |
r.event !== undefined && | |
['rsv-active', 'class-event'].every((cls) => r.event.className.includes(cls)) && | |
r.event.title.search(/karty zniżkowe/i) !== -1 && | |
calendar.date.isSame(r.event.start, 'day') | |
// document.contains(getp(r, ['element', 0])) | |
)) [0][1] | |
) | |
create_client = ({last_name, first_name}) => ( | |
$.ajax({ | |
type: 'GET', url: '/clients/create/client/', dataType: 'json', | |
}).then((data)=> { | |
let csrf = (data.html.match(/<input type='hidden' name='csrfmiddlewaretoken' value='(.+?)'/) || [null, null])[1] | |
if (!csrf) { | |
throw new Error('no csrf token found' + JSON.stringify(data)) | |
} | |
return $.ajax({type: 'POST', url: '/clients/create/client/', dataType: 'json', data: { | |
csrfmiddlewaretoken: csrf, | |
annotation: '', | |
first_name: first_name, | |
last_name: last_name, | |
email: '', | |
phone_number: '', | |
discount: 0, | |
enable_sms_notifications: 'on', | |
enable_creating_reservations: 'on', | |
is_new_player: 'on', | |
birth_date: '', | |
address: '', | |
zip_code: '', | |
venue: venue.id, // global | |
}}) | |
}).then((res) => ( | |
{id: Number(res.client_info_url.match(/\/clients\/client\/(\d+)\//)[1])} | |
)) | |
) | |
// inject a method to be called when an element | |
// <xyz smartforms-action="inject_custom_data"> | |
// is clicked. | |
// (see SmartForm.setHandlers and SmartForm.handleActionClick) | |
// when the user edits and saves the annotation, | |
// we need to reinject the extracted data. | |
// otherwise, all the custom data would be lost! | |
// IDEA: SmartForm.transitionTo(new_state) calls this[old_state+"2"+new_state] | |
// we could use this to inject some code to extract the stored data during init! | |
$.fn.smartform.Constructor.prototype.inject_custom_data = function(_event) { | |
// console.log('SmartForm.inject_custom_data', this, _event) | |
// let data = this.$element.data('get_custom_data')() | |
let data = this.get_custom_data() | |
this.inputElements().val((_i, ann) => { | |
let ann_text = get_annotation_text(ann) | |
let new_ann = serialize_annotation_data(ann_text, data) | |
console.log('new annotation', new_ann) | |
return new_ann | |
}) | |
// this.inputElements().val(new_ann) | |
} | |
is_class_reservation = (event) => event.client_list_url !== undefined | |
is_unavailability = (event) => event.className.includes('unavailability') | |
is_single_reservation = (event) => !is_class_reservation(event) && !is_unavailability(event) | |
// custom input stuff | |
function CustomInput(user_entries, props) { | |
this.state = this.initial_state(user_entries) | |
this.props = props | |
} | |
(() => { | |
CustomInput.prototype.initial_state = function(user_entries) { | |
return { | |
state: 'search', | |
user: null, | |
lastname: null, | |
firstname: null, | |
card: false, | |
user_entries: user_entries, | |
} | |
} | |
CustomInput.prototype.set_state = function(updates) { | |
let old_state = this.state | |
this.state = Object.assign({}, old_state, updates) | |
this.rerender() | |
if ('user_entries' in updates && !Object.is(old_state.user_entries, this.state.user_entries)) { | |
this.client_entries_render() | |
this.props.on_user_entries_change(this.state.user_entries) | |
} | |
} | |
CustomInput.prototype.rerender = function() { | |
let state = this.state.state | |
// console.log('CustomInput.rerender', state) | |
if (state === 'search') { | |
let user = this.state.user | |
this.elems.$search.show() | |
this.elems.$create.hide() | |
if (user === null) { | |
this.elems.$submit.prop('disabled', true) | |
this.elems.$clear.hide() | |
} else { | |
this.elems.$submit.prop('disabled', false) | |
this.elems.$clear.show() | |
} | |
} else if (state === 'create') { | |
this.elems.$search.hide() | |
this.elems.$create.show() | |
let lastname = this.state.lastname | |
let firstname = this.state.firstname | |
if (!firstname || !lastname) { | |
this.elems.$submit.prop('disabled', true) | |
this.elems.$clear.hide() | |
} else { | |
this.elems.$submit.prop('disabled', false) | |
this.elems.$clear.show() | |
} | |
} else { | |
throw new Error(`Invalid custom_input state: "${state}"`) | |
} | |
} | |
CustomInput.prototype.switch_to_create = function({firstname = '', lastname = ''}) { | |
this.set_state({state: 'create', firstname: firstname, lastname: lastname}) | |
this.elems.$lastname.val(lastname).focus() | |
this.elems.$firstname.val(firstname) | |
// this.elems.$create.change() | |
this.rerender() | |
} | |
CustomInput.prototype.clear = function() { | |
let state = this.state.state | |
// if (state === 'search') { | |
this.elems.$search.val('') | |
this.elems.$search.userProfileAutocomplete2('search', '') | |
// } else if (state === 'create') { | |
this.elems.$create.val('') | |
// } else { | |
// throw new Error(`Invalid custom_input state: "${state}"`) | |
// } | |
this.elems.$card.prop('checked', false) | |
this.set_state(this.initial_state(this.state.user_entries)) | |
} | |
CustomInput.prototype.render = function() { | |
let $elem = $(` | |
<div class="custom-wrapper divWrapper setPaddingLR setPaddingTB" data-extracted="null"> | |
<h5 class="gray" style="margin-bottom: 5px">Gracze</h5> | |
<div class="divWrapper"> | |
<div class="custom-add-user" style="display:flex; flex-direction: row"> | |
<input class="custom-add-user-search form-control" type="text" placeholder="Nazwisko klienta lub numer telefonu" style="border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-right: none;"> | |
<input class="custom-add-user-create custom-add-user-create-lastname form-control" type="text" placeholder="Nazwisko" style="flex-basis:40%; min-width: 0; border-top-right-radius: 0px; border-bottom-right-radius: 0px; border-right: none;"> | |
<input class="custom-add-user-create custom-add-user-create-firstname form-control" type="text" placeholder="Imię" style="flex-basis:40%; min-width: 0; border-radius: 0px;"> | |
<button class="custom-add-user-clear close" tabindex="-1" style="opacity: inherit; border-color: #e1e1e1; border-style: solid; border-width: 1px 0px;"> | |
× | |
</button> | |
<div style="flex-shrink: 0; padding-left: 0.5em; padding-right: 0.5em; border: 1px solid #e1e1e1; display: flex; align-items: center;"> | |
<span>MS</span> | |
<input type="checkbox" class="custom-add-user-card" style="width: auto; margin: initial; margin-left: 5px; display: inline;"> | |
</div> | |
<button class="custom-add-user-submit btn btn-primary" style="border-top-left-radius: 0px; border-bottom-left-radius: 0px; background: #3276b1; /*font-weight: bold;*/ padding: initial; line-height: initial; padding-left: 10px; padding-right: 10px; font-size: 23px;">+</button> | |
</div> | |
<ul class="custom-user-entry-list list-group" style="margin-top: 0.7em"></ul> | |
</div> | |
</div> | |
`) | |
$elem.find('.custom-user-entry-list') | |
this.elems = { | |
$root: $elem, | |
$search: $elem.find('.custom-add-user-search'), | |
$create: $elem.find('.custom-add-user-create'), | |
$submit: $elem.find('.custom-add-user-submit'), | |
$clear: $elem.find('.custom-add-user-clear'), | |
$lastname: $elem.find('.custom-add-user-create-lastname'), | |
$firstname: $elem.find('.custom-add-user-create-firstname'), | |
$card: $elem.find('.custom-add-user-card'), | |
$user_entry_list: $elem.find('.custom-user-entry-list'), | |
} | |
this.client_entries_render() | |
this.init_handlers() | |
this.rerender() | |
return this.elems.$root | |
} | |
CustomInput.prototype.client_entries_render = function() { | |
let state = this.state | |
let user_entry_list = this.elems.$user_entry_list | |
user_entry_list.empty().append( | |
_client_entries_render(state.user_entries) | |
) | |
this.client_entries_init_handlers() | |
return user_entry_list | |
} | |
const _client_entries_render = (user_entries) => ( | |
user_entries.map((entry, i) => | |
$('<li class="list-group-item"></li>').append(user_entry_render(entry, i)) | |
) | |
) | |
const user_entry_render = (entry, i) => ( | |
$(` | |
<div class="custom-user-entry" style="display:flex; justify-content: space-between; align-items: center;"> | |
<span style="flex-basis:100%"><a href="/clients/c/${entry.user.id}/" target="blank">${entry.user.label}</a></span> | |
${(entry.card) ? '<span class="badge" style="flex-basis: 15%;">MS</span>' : ''} | |
<button class="custom-user-entry-remove close" data-index="${i}" tabindex="-1" style="margin-left: 0.5em;"> | |
× | |
</button> | |
</div> | |
`) | |
) | |
CustomInput.prototype.client_entries_init_handlers = function() { | |
let user_entry_list = this.elems.$user_entry_list | |
user_entry_list.find('.custom-user-entry-remove').click((event) => { | |
let index = Number(event.target.getAttribute('data-index')) | |
this.set_state({user_entries: remove_index([...this.state.user_entries], index)}) | |
// this.modify_data((data) => { | |
// data.users.splice(index, 1) | |
// return data | |
// }) | |
}) | |
} | |
const remove_index = (xs, i) => {xs.splice(i, 1); return xs} | |
const append = (xs, x) => {xs.push(x); return xs} | |
const promise_pure = (x) => (new Promise((resolve)=>resolve(x))) | |
const title_case_word = (word) => ( | |
word ? word[0].toUpperCase() + word.substr(1).toLowerCase() : word | |
) | |
const parse_name = (s) => { | |
let parts = s.split(' ').map(title_case_word) | |
let lastname, firstname | |
if (!parts) { | |
lastname = '' | |
firstname = '' | |
} else if (parts.length == 1) { | |
lastname = parts[0] | |
firstname = '' | |
} else { | |
lastname = parts.slice(0, -1).join(' ') | |
firstname = parts[parts.length-1] | |
} | |
return [lastname, firstname] | |
} | |
// CustomInput.prototype.switch_to_search = function({user = null}) { | |
// this.state.state = 'search' | |
// this.state.user = user | |
// this.rerender() | |
// this.elems.$lastname.val(lastname).focus() | |
// this.elems.$firstname.val(firstname) | |
// this.elems.$create.change() | |
// this.rerender() | |
// } | |
CustomInput.prototype.submit = function() { | |
let state = this.state.state | |
let user_promise, card | |
if (state === 'search') { | |
card = this.state.card | |
user_promise = promise_pure(this.state.user) | |
} else if (state === 'create') { | |
card = this.state.card | |
let lastname = this.state.lastname | |
let firstname = this.state.firstname | |
// console.log('creating user', lastname, firstname) | |
user_promise = create_client({last_name: lastname, first_name: firstname}).then((client) => ( | |
{id: client.id, label: lastname + ' ' + firstname} | |
)) | |
} else { | |
throw new Error(`Invalid custom_input state: "${state}"`) | |
} | |
return user_promise.then((user) => { | |
let entry = {user: user, card: card} | |
this.set_state({user_entries: append([...this.state.user_entries], entry)}) | |
// this.modify_data((data) => { | |
// if (!('users' in data)) { data.users = [] } | |
// data.users.push(entry) | |
// return data | |
// }) | |
}) | |
} | |
CustomInput.prototype.init_handlers = function() { | |
this.elems.$search.userProfileAutocomplete2({ | |
showAddClient: true, | |
// withFunds: true, | |
minLength: 4, | |
focus: (evt, ui) => { | |
let item = ui.item | |
if ('is_add_new_client' in item) { | |
// set the input's value to the last thing searched. | |
// if the user had to down-arrow through a few options | |
// to choose "add new user", the input box would contain | |
// the last thing from the select list. | |
let search_input = $(evt.target) | |
let search_widget = search_input.data('customUserProfileAutocomplete2') | |
search_input.val(search_widget.term) | |
return false | |
} | |
}, | |
select: (evt, ui) => { | |
let item = ui.item | |
if ('is_add_new_client' in item) { | |
// create new client | |
// let search = evt.target.value | |
let search_widget = $(evt.target).data('customUserProfileAutocomplete2') | |
let search = search_widget.term | |
let [lastname, firstname] = parse_name(search) | |
this.switch_to_create({firstname: firstname, lastname: lastname}) | |
return false | |
} else { | |
// select existing client | |
let user = (item === null) ? null : {id: item.id, label: item.label} | |
this.set_state({user: user}) | |
} | |
} | |
}) | |
this.elems.$lastname.change((event) => { | |
let lastname = event.target.value | |
this.set_state({lastname: lastname}) | |
}) | |
this.elems.$firstname.change((event) => { | |
let firstname = event.target.value | |
this.set_state({firstname: firstname}) | |
}) | |
this.elems.$card.change((event) => { | |
let has_card = event.target.checked | |
this.set_state({card: has_card}) | |
}) | |
this.elems.$submit.click(() => { | |
this.submit().then(() => this.clear()) | |
}) | |
this.elems.$clear.click(() => { | |
this.clear() | |
}) | |
} | |
})() | |
if (typeof old_ReservationDetailsPopover === 'undefined') { | |
old_ReservationDetailsPopover = ReservationDetailsPopover | |
} | |
ReservationDetailsPopover = function(event_id, refetch_func, attachment_func) { | |
console.log('intercepted!', event_id) | |
var self = new old_ReservationDetailsPopover(event_id, refetch_func, attachment_func); | |
// console.log('self', self) | |
self.old_showPopover = self.showPopover | |
self.showPopover = function () { | |
self.old_showPopover() | |
let popover = self.popoverContent[0] | |
popover.focus() // doesn't seem to have any effect, maybe it's not visible yet? | |
} | |
self.show = self.showPopover // rebind alias | |
self.old_createPopoverContentFromResponse = self.createPopoverContentFromResponse | |
self.createPopoverContentFromResponse = function(data) { | |
// console.log('createPopoverContentFromResponse', self) | |
self.old_createPopoverContentFromResponse(data) | |
let popover = self.popoverContent[0] | |
let $popover = $(popover) | |
// self.selected_price = $popover.find('[data-price-list-id]').attr('data-price-list-id') | |
let event = $popover.data('event') | |
// console.log('event', event) | |
if (event !== undefined && !is_single_reservation(event)) { | |
// console.log('createPopoverContentFromResponse :: not a single_reservation, ignoring', event) | |
return | |
} | |
let $ann_tag = $(get_event_details_uninited_annotation_tag(popover)) | |
let [ann_text, ann_data] = parse_annotation_data($ann_tag.val()) | |
$ann_tag.val(ann_text) | |
ann_data = ann_data || {users: []} | |
let user_entries = ann_data.users | |
self.custom_input = new CustomInput(user_entries, { | |
on_user_entries_change: (user_entries) => ( | |
write_popover_data(self, user_entries ? {users: user_entries} : null) | |
) | |
}) | |
let el = $popover.children('.tab-content') | |
el.before(self.custom_input.render()) | |
// see inject_custom_data(...) above for explanation | |
let $ann_form = $ann_tag.closest('form') | |
let $save_ann = $ann_form.find('[type="submit"]') | |
$save_ann.attr('smartform-action', "inject_custom_data") | |
}; | |
self.old_afterReservationDetailsRender = self.afterReservationDetailsRender | |
self.afterReservationDetailsRender = () => { | |
// console.log('afterReservationDetailsRender', self) | |
self.old_afterReservationDetailsRender() | |
let popover = self.popoverContent[0] | |
let $popover = $(popover) | |
let event = $popover.data('event') | |
// console.log('event', event) | |
if (event !== undefined && !is_single_reservation(event)) { | |
// console.log('afterReservationDetailsRender :: not a single_reservation, event, ignoring', event) | |
return | |
} | |
let ann_sf = $(popover_annotation(self)).data('bs.smartform') | |
ann_sf.get_custom_data = () => { | |
let user_entries = self.custom_input.state.user_entries | |
return user_entries ? {users: user_entries} : null | |
} | |
// console.log('form', ann_sf) | |
// let $custom_input = $(popover).find('.custom-wrapper') | |
// this.init_state($custom_input) | |
// this.init($custom_input) | |
// this.update($custom_input) | |
} | |
return self | |
} | |
if (calendar.old_feedReservationCache === undefined) { | |
calendar.old_feedReservationCache = calendar.feedReservationCache | |
} | |
calendar.feedReservationCache = function(data) { | |
console.log('calendar.feedReservationCache', `${typeof data}: length ${data.length}`) | |
// PATCH | |
// if the user was logged out, api calls will redirect them to the login page | |
// and data will end up being that page's HTML. | |
// unfortunately, calendar.fetchReservations does no validation at all, so it gets through. | |
if (typeof data === "string") { | |
// error - needs relog | |
messages.appendMessage(Message.createMessage('Wymagane ponowne zalogowanie. Odśwież stronę', 'critical'), true); | |
return | |
} | |
return calendar.old_feedReservationCache( | |
data.map((event) => { | |
// watch out! freshly created events (i.e. the wizard is still open) don't have an id yet | |
event.price_info_promise = fetch_event_price_info(event.id) | |
return event | |
}) | |
) | |
} | |
if (ReservationEvent.prototype.old_collectAnnotations === undefined) { | |
ReservationEvent.prototype.old_collectAnnotations = ReservationEvent.prototype.collectAnnotations | |
} | |
ReservationEvent.prototype.collectAnnotations = function() { | |
let annotations = this.old_collectAnnotations() | |
return annotations.map((ann) => | |
(ann.type === 'unit') ? | |
(ann.annotation = get_annotation_text(ann.annotation), ann) : | |
ann | |
) | |
}; | |
count_cards = (ann_data) => { | |
if (ann_data) { | |
return ann_data.users.filter((e)=>e.card).length | |
} | |
return null | |
} | |
$(document).ready(() => { | |
const bootstrap = { | |
success: 'rgb(40, 167, 69)', // green | |
primary: 'rgb(0,123,255)', // blue | |
} | |
let head = $('head') | |
head.append($('<style id="card-info-badge-styles"></style>').text(` | |
.badge.card-info-badge { padding: 2px 5px; } | |
.badge.card-info-badge.loading { background-color: gray; opacity: 0.3; } | |
.badge.card-info-badge.guessed { opacity: 0.6; } | |
.badge.card-info-badge.missing { background-color: ${bootstrap.primary}; } | |
.badge.card-info-badge.fulfilled { background-color: gray; opacity: 0.3; } | |
.badge.card-info-badge.extra { background-color: ${bootstrap.success}; } | |
.badge.card-info-badge.error { background-color: gray; opacity: 0.5; } | |
`)) | |
// rgba(16, 76, 219, 0.9) // teal | |
// rgba(43, 25, 250, 0.7) // purple | |
let c = 'rgba(30, 54, 250, 0.85)' // deep purplish blue | |
head.append($('<style id="custom-styles"></style>').text(` | |
.rsv-unpaid.rsv-has-carnet { | |
box-shadow: inset 0px 5px 0px 0px ${c} !important; | |
} | |
`)) | |
}) | |
/* | |
badge: | |
loading {} | |
ready {used: int, required: int, paid: bool} | |
error {msg: str} | |
*/ | |
card_count_badge_state = ({used_cards_num, price_list_id, carnets, is_paid}) => { | |
const venue_price_info = VENUE_PRICE_INFO[venue.id] | |
let price_info = venue_price_info.prices[price_list_id] | |
if (price_info === undefined) { | |
return {type: 'error', msg: `Unknown price (price_list_id: ${price_list_id})`} | |
} else { | |
let required_cards_num | |
if (is_paid || (!is_paid && carnets === null)) { | |
required_cards_num = price_info.cards | |
} else { // !is_paid && carnets !== null | |
let carnet_type = Object.values(carnets)[0].type | |
let carnet_price_info = venue_price_info.carnets[carnet_type] | |
if (carnet_price_info === undefined) { | |
return {type: 'error', msg: `Unknown carnet (carnet_type: ${carnet_type})`} | |
} | |
required_cards_num = carnet_price_info.cards | |
} | |
return {type: 'ready', used: used_cards_num, required: required_cards_num, paid: is_paid} | |
} | |
} | |
card_count_badge_render = (state) => { | |
const make_badge = () => $(`<span class="card-info-badge badge badge-pill"></span>`) | |
let badge | |
if (state.type === 'loading') { | |
badge = make_badge() | |
badge .append(spinner_small()) .addClass('loading') | |
} else if (state.type === 'ready') { | |
let {used, required, paid} = state | |
if (required === 0 && used === 0) { | |
return $(null) | |
} | |
badge = make_badge() | |
if (used > required) { | |
badge .text(`${used - required}`) .addClass('extra') | |
} else if (used < required) { | |
badge .text(`${required - used}`) .addClass('missing') | |
} else { | |
badge .text('✓') .addClass('fulfilled') | |
} | |
if (!paid) { badge .addClass('guessed') } | |
} else if (state.type === 'error') { | |
let {msg} = state | |
badge = make_badge() | |
badge .text('×') .addClass('error') //.tooltip({title: msg, container: 'body'}) | |
} | |
return badge | |
} | |
spinner_small = () => { | |
let spinner = ich.spinner().css({width: '10px'}) | |
spinner.find('img').css({maxWidth: '100%'}) | |
return spinner | |
} | |
html_entities_decode = (s) => $("<div>").html(s).text() | |
fetch_event_price_info = (id) => ( | |
$.ajax({ | |
url: urlReverse('event_details'), data: { id: id }, | |
dataType: "json", | |
}).then((data) => { | |
let price_list_id = (data.html.match(/data-price-list-id="(\d+)"/) || [null, null])[1] | |
price_list_id = (price_list_id !== null && price_list_id !== "") ? Number(price_list_id) : null | |
let carnet_raw = (data.html.match(/data-carnets="(.+?)"/) || [null, null])[1] | |
let carnets = (carnet_raw !== null && carnet_raw !== '{}') | |
? JSON.parse(html_entities_decode(carnet_raw)) | |
: null | |
if (carnets === null) { | |
return {price_list_id: price_list_id, carnets: carnets} | |
} else { | |
let client_id = (data.html.match(/\/clients\/c\/(\d+)\//) || [null, null])[1] | |
if (client_id === null) { throw new Error(`no client id found for event ${id}`) } | |
// client_id = Number(client_id) | |
let reqs = Object.entries(carnets).map(([carnet_id, carnet]) => { | |
return $.ajax({ | |
type: 'GET', url: `/clients/carnet/create/${client_id}/`, dataType: 'json', | |
data: {carnet: carnet_id}, | |
}).then((data) => { | |
let carnet_type = (data.html.match(/<option value="(\d+)" price="[^"]+" days="[^"]+" resources="[^"]+" selected="selected">/) || [null, null])[1] | |
if (carnet_type === null) { throw new Error(`no carnet type found for user ${client_id}, carnet ${carnet_id}`) } | |
carnet.type = Number(carnet_type) | |
return [carnet_id, carnet] | |
}) | |
}) | |
return $.when(...reqs).then((...new_carnets) => ( | |
{price_list_id: price_list_id, carnets: Object.fromEntries(new_carnets)} | |
)) | |
} | |
}) | |
) | |
if (BaseReservationEvent.prototype.old_render === undefined) { | |
BaseReservationEvent.prototype.old_render = BaseReservationEvent.prototype.render | |
} | |
BaseReservationEvent.prototype.render = function(event, element) { | |
// console.log('BaseReservationEvent.render', this, event, element) | |
this.old_render(event, element) | |
if (event !== undefined && !is_single_reservation(event)) { | |
// console.log('BaseReservationEvent.render :: not a single_reservation, ignoring', event) | |
return | |
} | |
if (ReservationStub.prototype.isPrototypeOf(event) || event.className.includes('rsv-stub')) { | |
// reservation is being created | |
return | |
} | |
let title = this.element.find('.fc-event-title') | |
let title_text = title.find('div') | |
title.css({display: 'flex', justifyContent: 'flex-end', alignItems: 'baseline'}) | |
title_text.css({marginLeft: 'auto', marginRight: 'auto'}) | |
let annotation = this.event.annotation | |
let is_paid = this.event.className.includes('rsv-paid') | |
let ann_data = annotation ? get_annotation_data(annotation) : {users: []} | |
let used_cards_num = count_cards(ann_data) || 0 | |
// should only happen in development, during reloading | |
if (event.price_info_promise === undefined) { | |
event.price_info_promise = fetch_event_price_info(event.id) | |
} | |
let pending_badge = (event.price_info_promise.isResolved) | |
? $('<span></span>') | |
: card_count_badge_render({type: 'loading'}) | |
title.append(pending_badge) | |
event.price_info_promise.then((info) => { | |
console.log('fetched price info for', event.id, $(event.title).text(), '->', info) | |
let badge_state = card_count_badge_state({ | |
used_cards_num: used_cards_num, | |
is_paid: is_paid, | |
price_list_id: info.price_list_id, | |
carnets: info.carnets | |
}) | |
let badge = card_count_badge_render(badge_state) | |
pending_badge.replaceWith(badge) | |
if (info.carnets !== null) { | |
element.addClass('rsv-has-carnet') | |
} | |
}) | |
} | |
$.widget("custom.userProfileAutocomplete2", $.custom.userProfileAutocomplete, { | |
_renderMenu: function(ul, items) { | |
if (this.options.showAddClient) { | |
items.push({is_add_new_client: null, label: '', value: ''}) | |
} | |
ul.addClass('user-profile-autocomplete-menu'); | |
$.ui.autocomplete.prototype._renderMenu.call(this, ul, items); | |
}, | |
_renderItem: function(ul, item) { | |
if (this.options.showAddClient && 'is_add_new_client' in item) { | |
let el = ich.autocompleteAddNewClient() | |
ul.append(el); | |
return el | |
} else { | |
return $.ui.autocomplete.prototype._renderItem.call(this, ul, item) | |
} | |
}, | |
}); | |
VENUE_PRICE_INFO = { | |
80: { | |
"name": "SquashCity Jerozolimskie 200", | |
"class_prices": { | |
benefit: 1646, | |
}, | |
"price_default": 1707, | |
"prices": { | |
null: {"cards": 0, "name": "Własna cena"}, | |
1707: {"cards": 0, "name": "Cennik Standardowy"}, | |
1708: {"cards": 1, "name": "Mam 1 Kartę MultiSport Plus/FitProfit/OK System"}, | |
1709: {"cards": 2, "name": "Mam 2 Karty MultiSport Plus/FitProfit/OK System"}, | |
1710: {"cards": 1, "name": "Mam 1 Kartę OK System Gold"}, | |
1711: {"cards": 0, "name": "Karnet 5h"}, | |
1713: {"cards": 1, "name": "Karnet 5h - 1 karta zniżkowa"}, | |
1712: {"cards": 0, "name": "Karnet 10h"}, | |
1714: {"cards": 1, "name": "Karnet 10h - 1 karta zniżkowa"}, | |
1936: {"cards": 2, "name": "Karnet 10h - 2 karty zniżkowe"}, | |
1757: {"cards": 0, "name": "Juniorzy, Studenci (<26l) - 1h"}, | |
1758: {"cards": 1, "name": "Juniorzy, Studenci (<26l) - 1 karta zniżkowa"}, | |
1759: {"cards": 2, "name": "Juniorzy, Studenci (<26l) - 2 karty zniżkowe"}, | |
1760: {"cards": 0, "name": "Open Juniorzy, Studenci (<26 l) - karnet 5h"}, | |
1762: {"cards": 1, "name": "Open Juniorzy, Studenci (<26 l) - karnet 5h - jedna karta zniżkowa"}, | |
1761: {"cards": 0, "name": "Open Juniorzy, Studenci (<26 l) - karnet 10h"}, | |
1763: {"cards": 1, "name": "Open Juniorzy, Studenci (<26 l) - karnet 10h - jedna karta zniżkowa"}, | |
1754: {"cards": 0, "name": "Decathlon - cena hurtowa"}, | |
1755: {"cards": 0, "name": "Sekcja - cena hurtowa"}, | |
1756: {"cards": 0, "name": "Liga - cena hurtowa"}, | |
1918: {"cards": 0, "name": "Cennik trenerski"}, | |
3107: {"cards": 0, "name": "DoRozliczenia"}, | |
2380: {"cards": 0, "name": "__Goodie z kodem"}, | |
}, | |
"carnets": { | |
355: {"cards": 0, "name": "Karnet OPEN 5h, pn-pt 9-17"}, | |
356: {"cards": 0, "name": "Karnet OPEN 5h, pn-pt 17-23"}, | |
357: {"cards": 0, "name": "Karnet OPEN 5h, sb-nd"}, | |
358: {"cards": 0, "name": "Karnet OPEN 10h, pn-pt 9-17"}, | |
359: {"cards": 0, "name": "Karnet OPEN 10h, pn-pt 17-23"}, | |
360: {"cards": 0, "name": "Karnet OPEN 10h, sb-nd"}, | |
361: {"cards": 1, "name": "Karnet OPEN 5h, pn-pt 9-17 + 1 karta zniżkowa"}, | |
362: {"cards": 1, "name": "Karnet OPEN 5h, pn-pt 17-23 + 1 karta zniżkowa"}, | |
363: {"cards": 1, "name": "Karnet OPEN 5h, sb-nd + 1 karta zniżkowa"}, | |
364: {"cards": 1, "name": "Karnet OPEN 10h, pn-pt 9-17 + 1 karta zniżkowa"}, | |
365: {"cards": 1, "name": "Karnet OPEN 10h, pn-pt 17-23 + 1 karta zniżkowa "}, | |
366: {"cards": 1, "name": "Karnet OPEN 10h, sb-nd + 1 karta zniżkowa"}, | |
376: {"cards": 0, "name": "Karnet JUNIOR OPEN 5h, pn-pt 9-16"}, | |
476: {"cards": 0, "name": "Karnet OPEN 5h, pn-pt 7-9"}, | |
477: {"cards": 1, "name": "Karnet OPEN 5h, pn-pt 7-9 + 1 karta zniżkowa"}, | |
478: {"cards": 1, "name": "Karnet OPEN 10h, pn-pt 7-9 + 1 karta zniżkowa"}, | |
479: {"cards": 0, "name": "Karnet OPEN 10h, pn-pt 7-9"}, | |
480: {"cards": 1, "name": "Karnet JUNIOR OPEN 5h, pn-pt 9-16 + 1 karta zniżkowa"}, | |
481: {"cards": 1, "name": "Karnet JUNIOR OPEN 10h, pn-pt 9-16 + 1 karta zniżkowa"}, | |
482: {"cards": 0, "name": "Karnet JUNIOR OPEN 10h, pn-pt 9-16"}, | |
501: {"cards": 2, "name": "Karnet OPEN 10h, sb-nd + 2 karty zniżkowe"}, | |
502: {"cards": 2, "name": "Karnet OPEN 10h, pn-pt 7-9 + 2 karty zniżkowe"}, | |
503: {"cards": 2, "name": "Karnet OPEN 10h, pn-pt 17-23 + 2 karty zniżkowe"}, | |
504: {"cards": 2, "name": "Karnet OPEN 10h, pn-pt 9-17 + 2 karty zniżkowe"}, | |
}, | |
}, | |
82: { | |
"name": "SquashCity Targówek", | |
"price_default": 1540, | |
"prices": { | |
null: {"cards": 0, "name": "Własna cena"}, | |
1540: {"cards": 0, "name": "Standardowy"}, | |
1541: {"cards": 0, "name": "Juniorzy, Studenci (<26l) - 1h"}, | |
1542: {"cards": 1, "name": "1 karta Multisport Plus/FitProfit/OK System"}, | |
1543: {"cards": 2, "name": "2 karty Multisport Plus/FitProfit/OK System"}, | |
1547: {"cards": 0, "name": "Karnet 5h"}, | |
1539: {"cards": 1, "name": "Karnet 5h - 1 karta zniżkowa"}, | |
1959: {"cards": 0, "name": "Karnet 10h"}, | |
1960: {"cards": 1, "name": "Karnet 10h - 1 karta zniżkowa"}, | |
1961: {"cards": 2, "name": "Karnet 10h - 2 Karty zniżkowe"}, | |
2961: {"cards": 0, "name": "Karnet Liga"}, | |
3108: {"cards": 0, "name": "DoRozliczenia"}, | |
1617: {"cards": 0, "name": "Cennik trenerski"}, | |
2984: {"cards": 0, "name": "Korty dla Pań 25 zł (28-29.09.19)"}, | |
2985: {"cards": 1, "name": "Korty dla Pań z kartą zniżkową (28-29.09.19)"}, | |
2986: {"cards": 0, "name": "Korty dla Pań 50% taniej (27.09.19)"}, | |
2987: {"cards": 1, "name": "Korty dla Pań 50% taniej z kartą zniżkową (27.09.19)"}, | |
1546: {"cards": 0, "name": "XXX - do usunięcia: 7:00 - 17:00"}, | |
1548: {"cards": 0, "name": "XXX - do usunięcia: weekend 9:00 - 22:00"}, | |
1549: {"cards": 0, "name": "STARE: 1 wejście, OPEN"}, | |
1572: {"cards": 0, "name": "STARE: Zgrana paka, 1 wejście (pon-czw)"}, | |
}, | |
"carnets": { | |
380: {"cards": 0, "name": "Carnet OPEN 10h, pn-pt 7-17"}, | |
381: {"cards": 0, "name": "Carnet OPEN 10h, pn-pt 17-23"}, | |
382: {"cards": 0, "name": "Carnet OPEN 10h, sb-nd"}, | |
391: {"cards": 1, "name": "Carnet OPEN 10h, pn-pt 7-17 + 1 Karta zniżkowa"}, | |
392: {"cards": 1, "name": "Carnet OPEN 10h, pn-pt 17-23 + 1 Karta zniżkowa"}, | |
393: {"cards": 1, "name": "Carnet OPEN 10h, sb-nd + 1 Karta zniżkowa"}, | |
394: {"cards": 2, "name": "Carnet OPEN 10h, pn-pt 17-23 + 2 Karty zniżkowe"}, | |
395: {"cards": 2, "name": "Carnet OPEN 10h, sb-nd + 2 Karty zniżkowe"}, | |
377: {"cards": 0, "name": "Carnet OPEN 5h, pn-pt 7-17"}, | |
378: {"cards": 0, "name": "Carnet OPEN 5h, pn-pt 17-23"}, | |
379: {"cards": 0, "name": "Carnet OPEN 5h, sb-nd"}, | |
385: {"cards": 1, "name": "Carnet OPEN 5h, pn-pt 7-17 + 1 Karta zniżkowa"}, | |
386: {"cards": 1, "name": "Carnet OPEN 5h, pn-pt 17-23 + 1 Karta zniżkowa"}, | |
387: {"cards": 1, "name": "Carnet OPEN 5h, sb-nd + 1 Karta zniżkowa"}, | |
398: {"cards": 0, "name": "Carnet JUNIOR OPEN 10h, pn-pt 7-16"}, | |
397: {"cards": 1, "name": "Carnet JUNIOR OPEN 10h, pn-pt 7-16 + 1 Karta zniżkowa"}, | |
384: {"cards": 0, "name": "Carnet JUNIOR OPEN 5h"}, | |
383: {"cards": 0, "name": "Carnet JUNIOR OPEN 5h, pn-pt 7-16"}, | |
396: {"cards": 1, "name": "Carnet JUNIOR OPEN 5h, pn-pt 7-16 + 1 Karta zniżkowa"}, | |
306: {"cards": 0, "name": "Carnet OPEN 10h"}, | |
325: {"cards": 0, "name": "Carnet Zgrana Paka - Weekend"}, | |
326: {"cards": 0, "name": "Carnet Zgrana paka"}, | |
}, | |
}, | |
} | |
_refreshPopovers() | |
// calendar.updateFullcalendar() | |
calendar.fetchReservations() | |
calendar.HAS_ADD_CLIENTS = true | |
parse_class_reservation = (modal) => { | |
const parse_client = (el) => { | |
// sorry, the html is really ugly | |
// console.log(el) | |
let res = {} | |
res['reservation_id'] = null | |
res['status'] = ( | |
el.hasClass('rsv-active' ) ? 'active' : | |
el.hasClass('rsv-cancelled') ? 'cancelled' : | |
null | |
) | |
try { | |
let x = el.find('.panel-title .row').first().find('.col-lg-3') | |
// console.log(x) | |
let [_cancel, client, price, _presence] = x.toArray().map($) | |
let [collapse, client2] = client.find('a').toArray().map($) | |
res['client_name'] = (collapse.text()) | |
res['client_id'] = client2.attr('href').match(/\/clients\/c\/(\d+)\//)[1] | |
res['reservation_id'] = collapse.attr('href').match(/#event-reservation-collapse-(\d+)/)[1] | |
let price2 = price.find('.price').first() | |
// console.log('price', $.trim(price.text()), price) | |
// console.log('price2', price2.text(), price2) | |
res['reservation_price'] = ( | |
// standard -> `price` is an empty div | |
// cancelled -> `price` contains something like "reservation cancelled" | |
(price2.length === 0) ? ($.trim(price.text()) ? null : 'standard') : | |
(price2.text().search(/-15[,.]00/) !== -1) ? 'multisport' : | |
(price2.text().search(/-20[,.]00/) !== -1) ? 'ok_gold' : | |
null | |
) | |
// console.log(res['reservation_price']) | |
// console.log() | |
} catch (e) { | |
console.log(e) | |
} | |
return res | |
} | |
return modal.find('.fitnessPanel').toArray().map((el)=>parse_client($(el))) | |
} | |
class_event_reservations = (event_id) => { | |
return class_event_reservations_raw(event_id).then((text) => parse_class_reservation($(text))) | |
} | |
class_event_reservations_raw = (event_id) => { | |
return $.get(`https://reservise.com/events/class_event_reservations_list/${event_id}`) | |
} | |
group_by = (xs, f) => { | |
let g = {} | |
for (let x of xs) { | |
let fx = f(x) | |
if (!(fx in g)) { | |
g[fx] = [] | |
} | |
g[fx].push(x) | |
} | |
return g | |
} | |
// existing_num = Object.fromEntries(Object.entries(existing_by_client).map(([c,rs])=>[c,rs.length])) | |
// ann_num = Object.fromEntries(Object.entries(ann_ms_by_client).map(([c,rs])=>[c,rs.length])) | |
// x = [] | |
// for (let [c, n] of Object.entries(existing_num)) { | |
// let na = ann_num[c] || 0 | |
// if (na != n) { | |
// for(let i=0; i<n - na; i++) { | |
// x.push(existing_by_client[c][0].client_name) | |
// } | |
// } | |
// } | |
// x.sort() | |
async function sync_benefit(dry_run=true) { | |
let benefit_res_id = get_benefit_reservation().id | |
let existing = (await class_event_reservations(benefit_res_id)) .filter((r) => r.status === 'active') | |
console.log('before', existing) | |
// return | |
let existing_by_client = group_by(existing, (r)=>r.client_id) | |
let ann_ms = Object.values(calendar.reservationsById) | |
.filter((r) => r !== undefined && r.event !== undefined && calendar.date.isSame(r.event.start, 'day')) | |
.map((r) => get_annotation_data(r.event.annotation)) | |
.filter((d) => d!==null && d.users.length>0) | |
.flatMap((d) =>d.users) | |
.filter((e) => e.card) | |
console.log('from annotations', ann_ms) | |
// return | |
let ann_ms_by_client = group_by(ann_ms, (en)=>en.user.id) | |
for (let [client_id, entries] of Object.entries(ann_ms_by_client)) { | |
let existing_reservations = existing_by_client[client_id] || [] | |
let diff = entries.length - existing_reservations.length | |
let client_name = entries[0].user.label | |
console.log(client_id, client_name, 'needs', diff) | |
if (dry_run) { continue } | |
for (let i = 0; i < diff; i++) { | |
console.log('adding', client_name, entries[0].user.label) | |
let res = await add_benefit_reservation(client_id, benefit_res_id) | |
console.log(res) | |
if (res.success === undefined || !res.success) { | |
console.log('aborting') | |
return | |
} | |
} | |
} | |
} | |
// let KTOS_KTOS = 687826 | |
add_benefit_reservation = (client_id, benefit_res_id=null) => { | |
benefit_res_id = (benefit_res_id === null) ? get_benefit_reservation().id : benefit_res_id | |
let add_client_url = `/events/create_class_reservation/?event_id=${benefit_res_id}` | |
return $.ajax({type: 'GET', url: add_client_url, dataType: 'json', }).then((data) => { | |
let csrf = (data.html.match(/<input type='hidden' name='csrfmiddlewaretoken' value='(.+?)'/) || [null, null])[1] | |
if (!csrf) { | |
throw new Error('no csrf token found' + JSON.stringify(data)) | |
} | |
return $.ajax({type: 'POST', url: '/events/create_class_reservation/', dataType: 'json', data: { | |
csrfmiddlewaretoken: csrf, | |
event_id: benefit_res_id, | |
user_profile: client_id, | |
price_list: '15.00;'+VENUE_PRICE_INFO[venue.id].class_prices.benefit, | |
value: '15.00', | |
last_name: '', | |
first_name: '', | |
phone_number: '', | |
email: '', | |
annotation: '', | |
create_new_client: '', | |
}}) | |
}) | |
} | |
// await add_benefit_reservation(KTOS_KTOS).then((res)=>calendar.reservationsUpdated(res.updatedReservations)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment