diff --git a/admin.py b/admin.py index 3bbbd96..cc8d3aa 100644 --- a/admin.py +++ b/admin.py @@ -1,5 +1,6 @@ from sanic import response, redirect, Blueprint, exceptions from email_util import send_missing_propic_message +from random import choice from room import unconfirm_room_by_order from config import * from utils import * @@ -8,6 +9,8 @@ from io import StringIO from sanic.log import logger import csv import time +import json +import math bp = Blueprint("admin", url_prefix="/manage/admin") @@ -101,6 +104,179 @@ async def rename_room(request, code, order:Order): await dOrder.send_answers() return redirect(f'/manage/nosecount') +@bp.get('/room/wizard') +async def room_wizard(request, order:Order): + '''Tries to autofill unconfirmed rooms and other matches together''' + # Clear cache first + await clear_cache(request, order) + + #Separate orders which have incomplete rooms and which have no rooms + all_orders = {key:value for key,value in sorted(request.app.ctx.om.cache.items(), key=lambda x: len(x[1].room_members), reverse=True) if value.status not in ['c', 'e'] and not value.daily} + orders = {key:value for key,value in sorted(all_orders.items(), key=lambda x: x[1].ans('fursona_name')) if not value.room_confirmed} + # Orders with incomplete rooms + incomplete_orders = {key:value for key,value in orders.items() if value.code == value.room_id and (value.room_person_no - len(value.room_members)) > 0} + # Roomless furs + roomless_orders = {key:value for key,value in orders.items() if not value.room_id and not value.daily} + + # Result map + result_map = {} + + # Check overflows + room_quota_overflow = {} + for key, value in ITEM_VARIATIONS_MAP['bed_in_room'].items(): + room_quota = get_quota(ITEMS_ID_MAP['bed_in_room'], value) + capacity = ROOM_CAPACITY_MAP[key] if key in ROOM_CAPACITY_MAP else 1 + current_quota = len(list(filter(lambda y: y.bed_in_room == value and y.room_owner == True, orders.values()))) + room_quota_overflow[value] = current_quota - int(room_quota.size / capacity) if room_quota else 0 + + # Init rooms to remove + result_map["void"] = [] + + # Remove rooms that are over quota + for room_type, overflow_qty in {key:value for key,value in room_quota_overflow.items() if value > 0}.items(): + sorted_rooms = sorted(incomplete_orders.values(), key=lambda r: len(r.room_members)) + for room_to_remove in sorted_rooms[:overflow_qty]: + # Room codes to remove + result_map["void"].append(room_to_remove.code) + # Move room members to the roomless list + for member_code in room_to_remove.room_members: + roomless_orders[member_code] = all_orders[member_code] + del incomplete_orders[room_to_remove.code] + + # Fill already existing rooms + for room_order in incomplete_orders.items(): + room = room_order[1] + to_add = [] + missing_slots = room.room_person_no - len(room.room_members) + for i in range(missing_slots): + compatible_roomates = {key:value for key,value in roomless_orders.items() if value.bed_in_room == room.bed_in_room} + if len(compatible_roomates.items()) == 0: break + # Try picking a roomate that's from the same country and room type + country = room.country.lower() + roomless_by_country = {key:value for key,value in compatible_roomates.items() if value.country.lower() == country} + if len(roomless_by_country.items()) > 0: + code_to_add = list(roomless_by_country.keys())[0] + to_add.append(code_to_add) + del roomless_orders[code_to_add] + else: + # If not, add first roomless there is + code_to_add = list(compatible_roomates.keys())[0] + to_add.append(code_to_add) + del roomless_orders[code_to_add] + result_map[room.code] = { + 'type': 'add_existing', + 'to_add': to_add + } + + generated_counter = 0 + # Create additional rooms + while len(roomless_orders.items()) > 0: + room = list(roomless_orders.items())[0][1] + to_add = [] + missing_slots = room.room_person_no - len(room.room_members) + for i in range(missing_slots): + compatible_roomates = {key:value for key,value in roomless_orders.items() if value.bed_in_room == room.bed_in_room} + if len(compatible_roomates.items()) == 0: break + # Try picking a roomate that's from the same country and room type + country = room.country.lower() + roomless_by_country = {key:value for key,value in compatible_roomates.items() if value.country.lower() == country} + if len(roomless_by_country.items()) > 0: + code_to_add = list(roomless_by_country.keys())[0] + to_add.append(code_to_add) + del roomless_orders[code_to_add] + else: + # If not, add first roomless there is + code_to_add = list(compatible_roomates.keys())[0] + to_add.append(code_to_add) + del roomless_orders[code_to_add] + generated_counter += 1 + result_map[room.code] = { + 'type': 'new', + 'room_name': f'Generated Room {generated_counter}', + 'room_type': room.bed_in_room, + 'to_add': to_add + } + + result_map["infinite"] = { 'to_add': [] } + tpl = request.app.ctx.tpl.get_template('wizard.html') + return html(tpl.render(order=order, all_orders=all_orders, unconfirmed_orders=orders, data=result_map, jsondata=json.dumps(result_map, skipkeys=True, ensure_ascii=False))) + +@bp.post('/room/wizard/submit') +async def submit_from_room_wizard(request:Request, order:Order): + '''Will apply changes to the rooms''' + await request.app.ctx.om.fill_cache() + + data = json.loads(request.body) + + # Phase 1 - Delete all rooms in void + if 'void' in data: + for room_code in data['void']: + ppl = await get_people_in_room_by_code(request, room_code) + for p in ppl: + await p.edit_answer('room_id', None) + await p.edit_answer('room_confirmed', "False") + await p.edit_answer('room_name', None) + await p.edit_answer('pending_room', None) + await p.edit_answer('pending_roommates', None) + await p.edit_answer('room_members', None) + await p.edit_answer('room_owner', None) + await p.edit_answer('room_secret', None) + await p.send_answers() + logger.info(f"Deleted rooms {', '.join(data['void'])}") + + # Phase 2 - Join roomless to other rooms or add new rooms + for room_code, value in {key:value for key,value in data.items() if key.lower() not in ['void', 'infinite']}.items(): + if not value['to_add'] or len(value['to_add']) == 0: continue + room_order = await request.app.ctx.om.get_order(code=room_code) + # Preconditions + if not room_order: raise exceptions.BadRequest(f"Order {room_code} does not exist.") + if room_order.daily == True: raise exceptions.BadRequest(f"Order {room_code} is daily.") + if room_order.status != 'paid': raise exceptions.BadRequest(f"Order {room_code} hasn't paid.") + if room_order.room_owner: + if room_order.room_person_no < len(room_order.room_members) + (len(value['to_add']) if value['to_add'] else 0): + raise exceptions.BadRequest(f"Input exceeds room {room_order.code} capacity.") + elif room_order.room_person_no < (len(value['to_add']) if value['to_add'] else 0): + raise exceptions.BadRequest(f"Input exceeds room {room_order.code} capacity.") + + # Adding roomless orders to existing rooms + if value['type'] == 'add_existing' or value['type'] == 'new': + if value['type'] == 'new': + if room_order.room_owner: exceptions.BadRequest(f"Order {room_code} is already a room owner.") + # Create room data + await room_order.edit_answer('room_name', value['room_name']) + await room_order.edit_answer('room_id', room_order.code) + await room_order.edit_answer('room_secret', ''.join(choice('0123456789') for _ in range(6))) + elif not room_order.room_owner: + raise exceptions.BadRequest(f"Order {room_code} is not a room owner.") + # Add members + for new_member_code in value['to_add']: + pending_member = await request.app.ctx.om.get_order(code=new_member_code) + # Preconditions + if pending_member.daily == True: raise exceptions.BadRequest(f"Order {pending_member.code} is daily.") + if pending_member.status != 'paid': raise exceptions.BadRequest(f"Order {new_member_code} hasn't paid.") + if pending_member.bed_in_room != room_order.bed_in_room: raise exceptions.BadRequest(f"Order {new_member_code} has a different room type than {room_code}.") + if pending_member.room_owner: exceptions.BadRequest(f"Order {new_member_code} is already a room owner.") + if pending_member.room_id and pending_member.room_id not in data['void']: exceptions.BadRequest(f"Order {new_member_code} is in another room.") + await pending_member.edit_answer('room_id', room_order.code) + await pending_member.edit_answer('room_confirmed', "True") + await pending_member.edit_answer('pending_room', None) + await pending_member.send_answers() + logger.info(f"{'Created' if value['type'] == 'new' else 'Edited'} {str(room_order)}") + # Confirm members that were already inside the room + if value['type'] == 'add_existing': + for already_member in list(filter(lambda rm: rm.code in room_order.room_members and rm.code != room_order.code, request.app.ctx.om.cache.values())): + await already_member.edit_answer('room_confirmed', "True") + await already_member.send_answers() + else: raise exceptions.BadRequest(f"Unexpected type ({value['type']})") + await room_order.edit_answer('pending_room', None) + await room_order.edit_answer('pending_roommates', None) + await room_order.edit_answer('room_confirmed', "True") + await room_order.edit_answer('room_members', ','.join(list(set([*room_order.room_members, room_order.code, *value['to_add']])))) + await room_order.send_answers() + await request.app.ctx.om.fill_cache() + return text('done', status=200) + + @bp.get('/propic/remind') async def propic_remind_missing(request, order:Order): await clear_cache(request, order) diff --git a/app.py b/app.py index 51e2088..065116e 100644 --- a/app.py +++ b/app.py @@ -49,7 +49,7 @@ async def handleException(request, exception): statusCode = exception.status_code if hasattr(exception, 'status_code') else 500 try: tpl = app.ctx.tpl.get_template('error.html') - r = html(tpl.render(exception=exception, status_code=statusCode)) + r = html(tpl.render(exception=exception, status_code=statusCode), status=statusCode) except: traceback.print_exc() @@ -98,6 +98,11 @@ async def gen_barcode(request, code): return raw(img.getvalue(), content_type="image/png") +@app.route("/manage/lol") +async def lol(request: Request): + await get_quotas(request) + return text('hi') + @app.route(f"/{ORGANIZER}/{EVENT_NAME}/order///open/") async def redirect_explore(request, code, secret, order: Order, secret2=None): diff --git a/config.example.py b/config.example.py index 247ebec..acde031 100644 --- a/config.example.py +++ b/config.example.py @@ -65,6 +65,9 @@ SPONSORSHIP_COLOR_MAP = { 'normal': (142, 36, 170) } +# Quotes +QUOTES_LIST = [] + # Maps Products metadata name <--> ID ITEMS_ID_MAP = { 'early_bird_ticket': None, diff --git a/ext.py b/ext.py index 8db2788..7d0a35c 100644 --- a/ext.py +++ b/ext.py @@ -247,6 +247,44 @@ class Order: def get_language(self): return self.country.lower() if self.country.lower() in AVAILABLE_LOCALES else 'en' + + def __str__(self): + to_return = f"{'Room' if self.room_owner else 'Order'} {self.code}" + if self.room_owner == True: + to_return = f"{to_return} [ members = {self.room_members} ]" + return to_return + + def __repr__(self): + to_return = f"{'Room' if self.room_owner == True else 'Order'} {self.code}" + if self.room_owner == True: + to_return = f"{to_return} [ members = {self.room_members} ]" + return to_return + +@dataclass +class Quota: + def __init__(self, data): + self.items = data['items'] if 'items' in data else [] + self.variations = data['variations'] if 'variations' in data else [] + self.available = data['available'] if 'available' in data else False + self.size = data['size'] if 'size' in data else 0 + self.available_number = data['available_number'] if 'available_number' in data else 0 + + def has_item (self, id: int=-1, variation: int=None): + return id in self.items if not variation else (id in self.items and variation in self.variations) + + def get_left (self): + return self.available_number + + def __repr__(self): + return f'Quota [items={self.items}, variations={self.variations}] [{self.available_number}/{self.size}]' + + def __str__(self): + return f'Quota [items={self.items}, variations={self.variations}] [{self.available_number}/{self.size}]' + +def get_quota(item: int, variation: int = None) -> Quota: + for q in QUOTA_LIST: + if (q.has_item(item, variation)): return q + return None @dataclass class Quotas: @@ -265,6 +303,21 @@ async def get_quotas(request: Request=None): return Quotas(res) +async def load_item_quotas() -> bool: + global QUOTA_LIST + QUOTA_LIST = [] + logger.info ('[QUOTAS] Loading quotas...') + success = True + try: + res = await pretixClient.get('quotas/?order=id&with_availability=true') + res = res.json() + for quota_data in res['results']: + QUOTA_LIST.append (Quota(quota_data)) + except Exception: + logger.warning(f"[QUOTAS] Error while loading quotas.\n{traceback.format_exc()}") + success = False + return success + async def get_order(request: Request=None): await request.receive_body() return await request.app.ctx.om.get_order(request=request) @@ -328,6 +381,12 @@ class OrderManager: logger.error("[CACHE] Questions were not loading correctly. Aborting filling cache...") return False + # Load quotas + r = await load_item_quotas() + if(not r and check_itemsQuestions): + logger.error("[CACHE] Quotas were not loading correctly. Aborting filling cache...") + return False + cache = {} orderList = [] success = True @@ -363,7 +422,7 @@ class OrderManager: asyncio.create_task(validate_rooms(None, rooms, self)) return success - + async def get_order(self, request=None, code=None, secret=None, nfc_id=None, cached=False): # if it's a nfc id, just retorn it @@ -405,4 +464,4 @@ class OrderManager: if request and secret != res['secret']: raise exceptions.Forbidden("Your session has expired due to a token change. Please check your E-Mail for an updated link!") - return order + return order \ No newline at end of file diff --git a/res/botbg2.png b/res/botbg2.png deleted file mode 100644 index 632657c..0000000 Binary files a/res/botbg2.png and /dev/null differ diff --git a/res/error_openbox.png b/res/error_openbox.png deleted file mode 100644 index b54a081..0000000 Binary files a/res/error_openbox.png and /dev/null differ diff --git a/res/icons/book-plus.svg b/res/icons/book-plus.svg new file mode 100644 index 0000000..7d014d2 --- /dev/null +++ b/res/icons/book-plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/scripts/roomManager.js b/res/scripts/roomManager.js index 29537bf..a2ff674 100644 --- a/res/scripts/roomManager.js +++ b/res/scripts/roomManager.js @@ -1,11 +1,11 @@ function confirmAction (intent, sender) { if (['rename', 'unconfirm', 'delete'].includes (intent) == false) return let href = sender.getAttribute('action') - let intentTitle = document.querySelector("#intentText") - let intentEdit = document.querySelector("#intentRename") - let intentEditPanel = document.querySelector("#intentEditPanel") + let intentTitle = document.querySelector("#modalOrderEditDialog #intentText") + let intentEdit = document.querySelector("#modalOrderEditDialog #intentRename") + let intentEditPanel = document.querySelector("#modalOrderEditDialog #intentEditPanel") let intentFormAction = document.querySelector("#intentFormAction") - let intentSend = document.querySelector("#intentSend") + let intentSend = document.querySelector("#modalOrderEditDialog #intentSend") // Resetting ui intentEdit.removeAttribute('required') intentEdit.removeAttribute('minlength') @@ -27,5 +27,5 @@ function confirmAction (intent, sender) { case 'delete': break } - document.getElementById('modalRoomconfirm').setAttribute('open', 'true'); + document.getElementById('modalOrderEditDialog').setAttribute('open', 'true'); } \ No newline at end of file diff --git a/res/scripts/wizardManager.js b/res/scripts/wizardManager.js new file mode 100644 index 0000000..af63ea3 --- /dev/null +++ b/res/scripts/wizardManager.js @@ -0,0 +1,213 @@ +var draggingData = { + id: 0, + roomTypeId: 0, + parentRoomId: 0 +} + +var allowRedirect = false; + +function initObjects (){ + draggables = document.querySelectorAll("div.grid.people div.edit-drag"); + rooms = document.querySelectorAll("main.container>div.room"); + Array.from(draggables).forEach(element => { + element.addEventListener('dragstart', dragStart); + element.addEventListener('dragend', dragEnd); + }); + Array.from(rooms).forEach(room => { + room.addEventListener('dragenter', dragEnter) + room.addEventListener('dragover', dragOver); + room.addEventListener('dragleave', dragLeave); + room.addEventListener('drop', drop); + }); +} + +/** + * + * @param {DragEvent} e + */ +function dragStart(e) { + element = e.target; + room = element.closest('div.room') + setData(element.id, element.getAttribute('room-type'), room.id) + e.dataTransfer.effectAllowed = 'move'; + setTimeout(()=>toggleRoomSelection(true), 0); +} + +function dragEnd(e) { + toggleRoomSelection(false); + resetData (); + e.stopPropagation(); +} + +function dragEnter(e) { + e.preventDefault(); + e.target.classList.add('drag-over'); + checkDragLocation (getData(), e.target); + e.stopPropagation(); +} + +function dragOver(e) { + e.preventDefault(); + e.target.classList.add('drag-over'); + checkDragLocation (getData(), e.target) + e.stopPropagation(); +} + +/** + * + * @param {Element} target + */ +function checkDragLocation (data, target) { + let toReturn = true; + const isInfinite = target.getAttribute("infinite"); + const maxSizeReached = target.getAttribute("current-size") >= target.getAttribute("room-size"); + const roomTypeMismatch = data.roomTypeId !== target.getAttribute("room-type"); + if (!isInfinite && (maxSizeReached || roomTypeMismatch)) { + target.classList.add('drag-forbidden'); + toReturn = false; + } else { + target.classList.remove('drag-forbidden'); + } + return toReturn; +} + +function dragLeave(e) { + e.target.classList.remove('drag-over'); + e.target.classList.remove('drag-forbidden'); +} + +function drop(e) { + e.target.classList.remove('drag-over'); + toggleRoomSelection(false); + if (checkDragLocation(getData(), e.target) === true) { + const data = getData(); + let item = document.getElementById (data.id) + let oldParent = document.getElementById (data.parentRoomId) + let newParent = e.target; + if (moveToRoom (data.id, data.parentRoomId.replace('room-',''), newParent.id.replace('room-','')) === true) { + let newParentContainer = newParent.querySelector('div.grid.people') + newParentContainer.appendChild (item); + let oldParentQty = parseInt(oldParent.getAttribute("current-size")) - 1; + let newParentQty = parseInt(newParent.getAttribute("current-size")) + 1; + let newParentCapacity = parseInt(newParent.getAttribute("room-size")); + oldParent.setAttribute("current-size", oldParentQty); + newParent.setAttribute("current-size", newParentQty); + oldParent.classList.remove('complete'); + if (newParentCapacity == newParentQty) newParent.classList.add('complete'); + // if owner of room is being moved, assign a new owner + if (data.parentRoomId.replace('room-','') == data.id) { + // find first owner + if (model[data.id][toAdd] && model[data.id][toAdd].length <= 0) return; + newOwner = model[data.id][toAdd][0] + changeOwner (data.id, newOwner) + oldParent.id = "room-" + newOwner + } + } + } +} + +function toggleRoomSelection(newStatus) { + rooms = document.querySelectorAll("div.room"); + Array.from(rooms).forEach(room=>{ + room.classList.toggle('interactless', newStatus); + room.classList.remove('drag-over'); + room.classList.remove('drag-forbidden'); + }) +} + +function setData (id, roomType, parentRoomId) { + draggingData.id = id; + draggingData.roomTypeId = roomType; + draggingData.parentRoomId = parentRoomId; +} + +function resetData (){ setData(0, 0, 0); } + +function getData () { return draggingData; } + +// This default onbeforeunload event +window.onbeforeunload = function(){ + if (!allowRedirect) return "Any changes to the rooms will be discarded." +} + +/* Model managing */ + +var model = saveData; + +const toAdd = "to_add"; + +function moveToRoom (order, from, to){ + if (!model) { console.error("Model is null", order, from, to); return false; } + if (!model[from]) { console.error("Parent is null", order, from, to); return false; } + if (!model[to]) { console.error("Destination is null", order, from, to); return false; } + if (!model[from][toAdd] || !model[from][toAdd].includes(order)) { console.error("Order is not in parent", order, from, to); return false; } + if (!model[to][toAdd]) model[to][toAdd] = []; + // Delete order from the original room + model[from][toAdd] = model[from][toAdd].filter (itm=> itm !== order) + // Add it to the destination room + model[to][toAdd].push (order); + return true; +} + +function changeOwner (from, to){ + if (!model) { console.error("Model is null", from, to); return false; } + if (!model[from]) { console.error("Parent is null", from, to); return false; } + if (model[to]) { console.error("Destination already exist", from, to); return false; } + model[to] = {...model[from]} + delete model[from] +} + +function onSave (){ + if (model['infinite'] && model['infinite'][toAdd] && model['infinite'][toAdd].length > 0) { + setTimeout(()=>{ + let roomItem = document.querySelector("#room-infinite"); + roomItem.scrollIntoView(); + roomItem.classList.add('drag-forbidden'); + setTimeout(()=>roomItem.classList.remove('drag-forbidden'), 3000); + }, 100); + } else { + document.getElementById('modalConfirmDialog').setAttribute('open', 'true'); + } +} + +/** + * + * @param {Element} element + */ +function submitData (element){ + if (element.ariaDisabled) return; + element.ariaDisabled = true; + element.setAttribute("aria-busy", true); + document.querySelector("#modalClose").setAttribute("disabled", true); + document.querySelector("#modalClose").style.display = 'none'; + // Create request + const xhr = new XMLHttpRequest(); + xhr.open('POST', '/manage/admin/room/wizard/submit', true); + xhr.withCredentials = true; + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.onreadystatechange = function() { + if (xhr.readyState === XMLHttpRequest.DONE) { + let popoverText = document.querySelector("#popover-status-text"); + let popoverStatus = document.querySelector("#popover-status"); + popoverStatus.classList.remove('status-error'); + popoverStatus.classList.remove('status-success'); + if (xhr.status === 200) { + // Handle correct redirect + popoverText.innerText = "Changes applied successfully. Redirecting..." + popoverStatus.classList.add('status-success'); + } else { + // Handle errors + let error = xhr.statusText; + popoverText.innerText = "Could not apply changes: " + error; + console.error('Error submitting data:', error); + popoverStatus.classList.add('status-error'); + } + popoverStatus.showPopover(); + allowRedirect = true; + setTimeout(()=>window.location.assign('/manage/admin'), 3000); + } + }; + xhr.send(JSON.stringify(model)); +} + +initObjects (); \ No newline at end of file diff --git a/res/styles/admin.css b/res/styles/admin.css index d1b0f9b..5385e04 100644 --- a/res/styles/admin.css +++ b/res/styles/admin.css @@ -3,6 +3,18 @@ div.room-actions { float: right; } +div.admin-actions-header { + container-name: room-actions; + float: unset !important; + max-height: 2rem; + margin: 1rem 0px; +} + +div.admin-actions-header img { + max-height: 1.5rem; +} + + div.room-actions > a { background-color: var(--card-background-color); font-size: 12pt; @@ -16,4 +28,26 @@ div.room-actions > a:hover { div.room-actions > a.act-del:hover { background-color: var(--del-color); +} + +/* Spinning animation */ +@keyframes spin { + from { + transform:rotate(0deg); + } + to { + transform:rotate(360deg); + } +} + +h3:has(.spin) { + overflow: hidden; +} + +.spin { + animation-name: spin; + animation-duration: 500ms; + animation-iteration-count: infinite; + animation-timing-function: linear; + max-height: 32px; } \ No newline at end of file diff --git a/res/styles/base.css b/res/styles/base.css index 37d9a1a..51bb7d9 100644 --- a/res/styles/base.css +++ b/res/styles/base.css @@ -27,6 +27,14 @@ summary:has(span.status) { 100% { background-position:57.75% 0%; } } +/* Popover */ +*[popover]:popover-open { + border-radius: var(--border-radius); + border: 1px solid #fff; + backdrop-filter: blur(10px); + padding: 1rem; +} + /* Dark theme */ @media only screen and (prefers-color-scheme: dark) { .icon {filter: invert(1);} diff --git a/res/styles/navbar.css b/res/styles/navbar.css index 4d34f22..43c99cd 100644 --- a/res/styles/navbar.css +++ b/res/styles/navbar.css @@ -14,7 +14,8 @@ nav#topbar { top: 0rem; transition: top 300ms; line-height: 2em; - max-width:98vw; + max-width:100%; + overflow-x: hidden; } nav#topbar a { diff --git a/res/styles/wizard.css b/res/styles/wizard.css new file mode 100644 index 0000000..d11a5ad --- /dev/null +++ b/res/styles/wizard.css @@ -0,0 +1,80 @@ +div.grid.people div.edit-disabled { + pointer-events: none; + filter: grayscale(1); + user-select: none; + cursor: not-allowed; +} + +div.grid.people div.edit-drag>div.propic-container { + pointer-events: none; +} + +div.drag-over { + border-color: #000; + border-style: dashed; +} + +div.drag-forbidden { + border-color: #c92121aa; +} + +div.interactless > * { + pointer-events: none; +} + +div.room.complete { + border-color: #21c929aa; + border-style: solid; +} + +div.room { + border-radius: var(--border-radius); + border: 1px solid transparent; + margin-bottom: var(--spacing); +} + +div.room > h4 { + user-select: none; + margin-left: 1.6rem; +} + +div.room:nth-child(2n) { + background-color: #ffffff55; +} + +div.room:nth-child(2n) { + background-color: #cccccc55; +} + +.align-right { + float: right; +} + +.status-success { + background-color: #2e9147aa; +} + +.status-error { + background-color: #912e2eaa; +} + +/* Dark theme */ +@media only screen and (prefers-color-scheme: dark) { + div.drag-over { + border-color: #fff; + border-style: dashed; + } + + div.drag-forbidden { + border-color: #c92121aa; + } + + div.room { + background-color: #16161655; + } + + div.room:nth-child(2n) { + background-color: #20202055; + } + +} \ No newline at end of file diff --git a/tpl/admin.html b/tpl/admin.html index 35627d1..409ab19 100644 --- a/tpl/admin.html +++ b/tpl/admin.html @@ -11,14 +11,20 @@

Admin panel

+

Data

Clear cache - Manage rooms - Verify Rooms - Remind badge upload - Auto-confirm Rooms Export CSV Export hotel CSV
+

Rooms

+ Manage rooms + Verify Rooms + Fill Rooms +
+

Profiles

+ Remind badge upload + Auto-confirm Rooms +
{% include 'components/confirm_action_modal.html' %} diff --git a/tpl/blocks/propic.html b/tpl/blocks/propic.html index ef7e950..e0261c3 100644 --- a/tpl/blocks/propic.html +++ b/tpl/blocks/propic.html @@ -7,7 +7,7 @@ {% endif %} - {% if current and current.isAdmin () and (not current.code == order.code )%} + {% if current and current.isAdmin () and (not current.code == order.code ) and not nologin %}

Confirm room edit

@@ -150,6 +152,5 @@
- {% endblock %} diff --git a/tpl/wizard.html b/tpl/wizard.html new file mode 100644 index 0000000..9cef6cb --- /dev/null +++ b/tpl/wizard.html @@ -0,0 +1,88 @@ +{% extends "base.html" %} +{% block title %}Room Wizard{% endblock %} +{% block head %} + +{% endblock %} +{% block main %} +
+ +
+ + + + +
+ +

Review rooms ?

+
This is the preview page. Re-arrange users by dragging and dropping them in the rooms.
Once finished, scroll down to either 'Confirm' changes or 'Undo' them.
+
+ {% for room in data.items() %} + {% if room[0] in all_orders %} + {%with room_order = unconfirmed_orders[room[0]] %} +
+

+ {{room_order.room_name if room_order.room_name else room[1]['room_name'] if room[1] and room[1]['room_name'] else ''}} +

+
+ {% for m in room_order.room_members %} + {% if m in all_orders %} + {% with person = all_orders[m] %} +
+ {% with current=None, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = false, flag = true %} + {% include 'blocks/propic.html' %} + {% endwith %} +
{{person.ans('fursona_name')}}
+
+ {% endwith %} + {% endif %} + {% endfor %} + {% for m in room[1]['to_add'] %} + {% if m in unconfirmed_orders %} + {% with person = unconfirmed_orders[m] %} +
+ {% with current=None, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = false, flag = true %} + {% include 'blocks/propic.html' %} + {% endwith %} +
{{person.ans('fursona_name')}}
+
+ {% endwith %} + {% endif %} + {% endfor %} +
+
+ {% endwith %} + {% endif %} + {% endfor %} +
+

Empty room ?

+
This is a placeholder room. Place users temporarily in order to free space and arrange rooms
+
+
+ Undo + Confirm changes + + +
+ +

Confirm arrangement?

+

+ Roomless guests will be moved around existing rooms and newly generated ones.
+ This will also confirm all rooms. +

+
+
+ +
+
+
+ + + +
+ +{% endblock %} diff --git a/utils.py b/utils.py index 0876148..e599fa7 100644 --- a/utils.py +++ b/utils.py @@ -29,7 +29,6 @@ QUESTION_TYPES = { #https://docs.pretix.eu/en/latest/api/resources/questions.htm } TYPE_OF_QUESTIONS = {} # maps questionId -> type - async def load_questions() -> bool: global TYPE_OF_QUESTIONS # TYPE_OF_QUESTIONS.clear() It should not be needed @@ -84,7 +83,7 @@ async def load_items() -> bool: ROOM_TYPE_NAMES[v['id']] = roomName # Adds itself to the category list categoryName = check_and_get_category ('item', q) - if not categoryName: continue + if not categoryName or q['id'] in CATEGORIES_LIST_MAP[categoryName]: continue CATEGORIES_LIST_MAP[categoryName].append(q['id']) if (EXTRA_PRINTS): logger.debug(f'Mapped Items: %s', ITEMS_ID_MAP) @@ -149,7 +148,7 @@ async def get_order_by_code(request, code, throwException=False): async def get_people_in_room_by_code(request, code, om=None): if not om: om = request.app.ctx.om await om.update_cache() - return filter(lambda rm: rm.room_id == code, om.cache.values()) + return list(filter(lambda rm: rm.room_id == code, om.cache.values())) async def confirm_room_by_order(order, request): bed_in_room = order.bed_in_room # Variation id of the ticket for that kind of room