Code cleanup, readme written
This commit is contained in:
parent
b1fb2bb105
commit
d53ee2eaf0
14
app.py
14
app.py
|
@ -29,12 +29,9 @@ from export import bp as export_bp
|
||||||
from stats import bp as stats_bp
|
from stats import bp as stats_bp
|
||||||
from api import bp as api_bp
|
from api import bp as api_bp
|
||||||
from carpooling import bp as carpooling_bp
|
from carpooling import bp as carpooling_bp
|
||||||
from nfc import bp as nfc_bp
|
|
||||||
from checkin import bp as checkin_bp
|
from checkin import bp as checkin_bp
|
||||||
from money import bp as money_bp
|
|
||||||
from boop import bp as boop_bp
|
|
||||||
|
|
||||||
app.blueprint([room_bp, karaoke_bp, propic_bp, export_bp, stats_bp, api_bp, carpooling_bp, nfc_bp, checkin_bp, money_bp, boop_bp])
|
app.blueprint([room_bp, karaoke_bp, propic_bp, export_bp, stats_bp, api_bp, carpooling_bp, checkin_bp])
|
||||||
|
|
||||||
@app.exception(exceptions.SanicException)
|
@app.exception(exceptions.SanicException)
|
||||||
async def clear_session(request, exception):
|
async def clear_session(request, exception):
|
||||||
|
@ -57,15 +54,9 @@ async def main_start(*_):
|
||||||
log.info("Cache fill done!")
|
log.info("Cache fill done!")
|
||||||
|
|
||||||
app.ctx.nfc_counts = sqlite3.connect('data/nfc_counts.db')
|
app.ctx.nfc_counts = sqlite3.connect('data/nfc_counts.db')
|
||||||
app.ctx.boop = sqlite3.connect('data/boop.db')
|
|
||||||
app.ctx.money = sqlite3.connect('data/money.db')
|
|
||||||
app.ctx.money.row_factory = sqlite3.Row
|
|
||||||
|
|
||||||
app.ctx.login_codes = {}
|
app.ctx.login_codes = {}
|
||||||
|
|
||||||
app.ctx.nfc_reads = {}
|
|
||||||
app.ctx.boops = Queue()
|
|
||||||
|
|
||||||
app.ctx.tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=True)
|
app.ctx.tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=True)
|
||||||
app.ctx.tpl.globals.update(time=time)
|
app.ctx.tpl.globals.update(time=time)
|
||||||
app.ctx.tpl.globals.update(PROPIC_DEADLINE=PROPIC_DEADLINE)
|
app.ctx.tpl.globals.update(PROPIC_DEADLINE=PROPIC_DEADLINE)
|
||||||
|
@ -80,7 +71,7 @@ async def gen_barcode(request, code):
|
||||||
|
|
||||||
return raw(img.getvalue(), content_type="image/png")
|
return raw(img.getvalue(), content_type="image/png")
|
||||||
|
|
||||||
@app.route("/furizon/beyond/order/<code>/<secret>/open/<secret2>")
|
@app.route(f"/{ORGANIZER}/{EVENT_NAME}/order/<code>/<secret>/open/<secret2>")
|
||||||
async def redirect_explore(request, code, secret, order: Order, secret2=None):
|
async def redirect_explore(request, code, secret, order: Order, secret2=None):
|
||||||
|
|
||||||
r = redirect(app.url_for("welcome"))
|
r = redirect(app.url_for("welcome"))
|
||||||
|
@ -89,6 +80,7 @@ async def redirect_explore(request, code, secret, order: Order, secret2=None):
|
||||||
if not order:
|
if not order:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
res = await client.get(join(base_url, f"orders/{code}/"), headers=headers)
|
res = await client.get(join(base_url, f"orders/{code}/"), headers=headers)
|
||||||
|
print(res.json())
|
||||||
if res.status_code != 200:
|
if res.status_code != 200:
|
||||||
raise exceptions.NotFound("This order code does not exist. Check that your order wasn't deleted, or the link is correct.")
|
raise exceptions.NotFound("This order code does not exist. Check that your order wasn't deleted, or the link is correct.")
|
||||||
|
|
||||||
|
|
86
boop.py
86
boop.py
|
@ -1,86 +0,0 @@
|
||||||
from sanic import Blueprint, exceptions, response
|
|
||||||
from time import time
|
|
||||||
from asyncio import Future
|
|
||||||
import asyncio
|
|
||||||
from datetime import datetime
|
|
||||||
from asyncio import Queue
|
|
||||||
from random import randint
|
|
||||||
from boop_process import boop_process
|
|
||||||
|
|
||||||
bp = Blueprint("boop", url_prefix="/boop")
|
|
||||||
|
|
||||||
bp.ctx.boopbox_queue = {'a': Queue(), 'b': Queue(), 'c': Queue()}
|
|
||||||
bp.ctx.busy = {'a': False, 'b': False, 'c': False}
|
|
||||||
bp.ctx.last_tag = {'a': None, 'b': None, 'c': None}
|
|
||||||
bp.ctx.repeats = {'a': None, 'b': None, 'c': None}
|
|
||||||
|
|
||||||
def enable_nfc(enable, boopbox_id):
|
|
||||||
log.info('NFC is ' + ('enabled' if enable else 'disabled'))
|
|
||||||
app.ctx.queue.put_nowait({'_': 'nfc', 'enabled': enable})
|
|
||||||
app.ctx.nfc_enabled = enable;
|
|
||||||
|
|
||||||
@bp.get("/refresh")
|
|
||||||
async def refresh_boops(request):
|
|
||||||
await boop_process(request.app.ctx.om.cache.values(), request.app.ctx.boop)
|
|
||||||
return response.text('ok')
|
|
||||||
|
|
||||||
@bp.get("/")
|
|
||||||
async def show_boopbox(request):
|
|
||||||
tpl = request.app.ctx.tpl.get_template('boopbox.html')
|
|
||||||
return response.html(tpl.render())
|
|
||||||
|
|
||||||
@bp.get("/getqueue/<boopbox_id>")
|
|
||||||
async def boop_queue(request, boopbox_id):
|
|
||||||
|
|
||||||
items = []
|
|
||||||
queue = bp.ctx.boopbox_queue[boopbox_id]
|
|
||||||
while 1:
|
|
||||||
try:
|
|
||||||
item = queue.get_nowait()
|
|
||||||
except asyncio.queues.QueueEmpty:
|
|
||||||
if items:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Try one last time to get a task, then fail.
|
|
||||||
bp.ctx.busy[boopbox_id] = False
|
|
||||||
try:
|
|
||||||
item = await asyncio.wait_for(queue.get(), timeout=5)
|
|
||||||
except asyncio.exceptions.TimeoutError:
|
|
||||||
break
|
|
||||||
|
|
||||||
items.append(item)
|
|
||||||
|
|
||||||
if len(items):
|
|
||||||
bp.ctx.busy[boopbox_id] = True
|
|
||||||
return response.json(items)
|
|
||||||
|
|
||||||
@bp.post("/read")
|
|
||||||
async def handle_boop(request):
|
|
||||||
payload = request.json
|
|
||||||
queue = bp.ctx.boopbox_queue[payload['boopbox_id']]
|
|
||||||
|
|
||||||
if bp.ctx.busy[payload['boopbox_id']]: return response.text('busy')
|
|
||||||
|
|
||||||
|
|
||||||
await queue.put({'_': 'play', 'src': f"/res/snd/error.wav"})
|
|
||||||
|
|
||||||
if bp.ctx.last_tag[payload['boopbox_id']] == payload['id']:
|
|
||||||
bp.ctx.repeats[payload['boopbox_id']] += 1
|
|
||||||
else:
|
|
||||||
bp.ctx.last_tag[payload['boopbox_id']] = payload['id']
|
|
||||||
bp.ctx.repeats[payload['boopbox_id']] = 0
|
|
||||||
|
|
||||||
if bp.ctx.repeats[payload['boopbox_id']] > 5:
|
|
||||||
await queue.put({'_': 'play', 'src': f"/res/snd/ratelimit.wav"})
|
|
||||||
await queue.put({'_': 'bye'})
|
|
||||||
return response.text('ok')
|
|
||||||
|
|
||||||
if bp.ctx.repeats[payload['boopbox_id']] > 10:
|
|
||||||
await queue.put({'_': 'talk', 'who': 'tiger', 'msg': f"Hey! Stop that! You're not the only one here!"})
|
|
||||||
await queue.put({'_': 'bye'})
|
|
||||||
return response.text('ok')
|
|
||||||
|
|
||||||
request.app.ctx.boop.execute('INSERT INTO boop(tag_id, station, ts) VALUES (?,?,?)', (payload['id'], payload['boopbox_id'], int(time())))
|
|
||||||
request.app.ctx.boop.commit()
|
|
||||||
|
|
||||||
return response.text('ok')
|
|
|
@ -1,42 +0,0 @@
|
||||||
from datetime import datetime
|
|
||||||
from random import randint
|
|
||||||
from hashlib import md5
|
|
||||||
from base64 import b16encode
|
|
||||||
|
|
||||||
fibonacci = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233]
|
|
||||||
current_date = datetime.today()
|
|
||||||
|
|
||||||
async def boop_process(orders, db):
|
|
||||||
print(f'Processing {len(orders)} orders')
|
|
||||||
db.execute('DELETE FROM player')
|
|
||||||
|
|
||||||
for o in orders:
|
|
||||||
|
|
||||||
tags = []
|
|
||||||
code = o.code
|
|
||||||
tag_id = o.nfc_id or b16encode(md5(code.encode()).digest()).decode()[:14]
|
|
||||||
birthday = o.birth_date
|
|
||||||
badge_id = o.badge_id or randint(0,200)
|
|
||||||
room = o.actual_room or randint(100,400)
|
|
||||||
country = o.country
|
|
||||||
name = o.first_name.lower().strip()
|
|
||||||
|
|
||||||
birth_date = datetime.strptime(birthday, "%Y-%m-%d").date()
|
|
||||||
|
|
||||||
age = current_date.year - birth_date.year
|
|
||||||
if (current_date.month, current_date.day) < (birth_date.month, birth_date.day):
|
|
||||||
age -= 1
|
|
||||||
|
|
||||||
# Check if the birthday is today
|
|
||||||
if (current_date.month, current_date.day) == (birth_date.month, birth_date.day):
|
|
||||||
tags.append('birthday')
|
|
||||||
|
|
||||||
# Check if the badge is a fib number
|
|
||||||
if badge_id in fibonacci:
|
|
||||||
tags.append('fibonacci')
|
|
||||||
|
|
||||||
db.execute('INSERT INTO player(tag_id, code, tags, birthday, badge_id, age, name, room, country) VALUES (?,?,?,?,?,?,?,?,?)',
|
|
||||||
(tag_id, code, ','.join(tags), birthday, badge_id, age, name, room, country)
|
|
||||||
)
|
|
||||||
|
|
||||||
db.commit()
|
|
Binary file not shown.
Binary file not shown.
BIN
data/event.db
BIN
data/event.db
Binary file not shown.
Binary file not shown.
Binary file not shown.
21
ext.py
21
ext.py
|
@ -34,7 +34,7 @@ class Order:
|
||||||
self.checked_in = False
|
self.checked_in = False
|
||||||
|
|
||||||
for p in self.data['positions']:
|
for p in self.data['positions']:
|
||||||
if p['item'] in [16, 38]:
|
if p['item'] in ITEM_IDS['ticket']:
|
||||||
self.position_id = p['id']
|
self.position_id = p['id']
|
||||||
self.position_positionid = p['positionid']
|
self.position_positionid = p['positionid']
|
||||||
self.answers = p['answers']
|
self.answers = p['answers']
|
||||||
|
@ -42,11 +42,11 @@ class Order:
|
||||||
self.address = f"{p['street']} - {p['zipcode']} {p['city']} - {p['country']}"
|
self.address = f"{p['street']} - {p['zipcode']} {p['city']} - {p['country']}"
|
||||||
self.checked_in = bool(p['checkins'])
|
self.checked_in = bool(p['checkins'])
|
||||||
|
|
||||||
if p['item'] == 17:
|
if p['item'] in ITEM_IDS['membership_card']:
|
||||||
self.has_card = True
|
self.has_card = True
|
||||||
|
|
||||||
if p['item'] == 19:
|
if p['item'] in ITEM_IDS['sponsorship']:
|
||||||
self.sponsorship = 'normal' if p['variation'] == 13 else 'super'
|
self.sponsorship = 'normal' if p['variation'] == ITEMS_IDS['sponsorship'][0] else 'super'
|
||||||
|
|
||||||
if p['country']:
|
if p['country']:
|
||||||
self.country = p['country']
|
self.country = p['country']
|
||||||
|
@ -55,10 +55,10 @@ class Order:
|
||||||
self.first_name = p['attendee_name_parts']['given_name']
|
self.first_name = p['attendee_name_parts']['given_name']
|
||||||
self.last_name = p['attendee_name_parts']['family_name']
|
self.last_name = p['attendee_name_parts']['family_name']
|
||||||
|
|
||||||
if p['item'] == 20:
|
if p['item'] == ITEM_IDS['early_arrival']:
|
||||||
self.has_early = True
|
self.has_early = True
|
||||||
|
|
||||||
if p['item'] == 21:
|
if p['item'] == ITEM_IDS['late_departure']:
|
||||||
self.has_late = True
|
self.has_late = True
|
||||||
|
|
||||||
self.total = float(data['total'])
|
self.total = float(data['total'])
|
||||||
|
@ -172,17 +172,10 @@ class Order:
|
||||||
class Quotas:
|
class Quotas:
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
self.data = data
|
self.data = data
|
||||||
self.capacity_mapping = {
|
|
||||||
1: 16,
|
|
||||||
2: 17,
|
|
||||||
3: 18,
|
|
||||||
4: 19,
|
|
||||||
5: 20
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_left(self, capacity):
|
def get_left(self, capacity):
|
||||||
for quota in self.data['results']:
|
for quota in self.data['results']:
|
||||||
if quota['id'] == self.capacity_mapping[capacity]:
|
if quota['id'] == ROOM_MAP[capacity]:
|
||||||
return quota['available_number']
|
return quota['available_number']
|
||||||
|
|
||||||
async def get_quotas(request: Request=None):
|
async def get_quotas(request: Request=None):
|
||||||
|
|
53
money.py
53
money.py
|
@ -1,53 +0,0 @@
|
||||||
from sanic import Blueprint, exceptions, response
|
|
||||||
from time import time
|
|
||||||
from asyncio import Future
|
|
||||||
import asyncio
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
bp = Blueprint("money", url_prefix="/money")
|
|
||||||
|
|
||||||
@bp.post("/pos")
|
|
||||||
async def do_transaction(request):
|
|
||||||
|
|
||||||
message = ''
|
|
||||||
tx_id = request.app.ctx.money.execute('INSERT INTO tx(tag_id, amount, ts) VALUES (?,?,?) RETURNING id', (request.form.get('nfc_id'), request.form.get('total'), time())).fetchone()[0]
|
|
||||||
|
|
||||||
for item, qty in request.form.items():
|
|
||||||
if not item.startswith('itm_'): continue
|
|
||||||
if qty[0] == '0': continue
|
|
||||||
request.app.ctx.money.execute('INSERT INTO tx_items(tx_id, item_id, qty) VALUES (?,?,?)', (tx_id, item[4:], qty[0]))
|
|
||||||
|
|
||||||
request.app.ctx.money.commit()
|
|
||||||
return await show_transactions(request, message='Transazione eseguita con successo!')
|
|
||||||
|
|
||||||
@bp.get("/pos")
|
|
||||||
async def show_transactions(request, message=None):
|
|
||||||
tpl = request.app.ctx.tpl.get_template('pos.html')
|
|
||||||
items = request.app.ctx.money.execute('SELECT * FROM item')
|
|
||||||
|
|
||||||
tx_info = {}
|
|
||||||
last_tx = request.app.ctx.money.execute('SELECT * FROM tx WHERE amount < 0 ORDER BY ts DESC LIMIT 3').fetchall()
|
|
||||||
for tx in last_tx:
|
|
||||||
tx_info[tx['id']] = {'items': request.app.ctx.money.execute('SELECT * FROM tx_items JOIN item ON item_id = item.id AND tx_id = ?', (tx['id'],)).fetchall(), 'order': await request.app.ctx.om.get_order(nfc_id=tx['tag_id']), 'time': datetime.fromtimestamp(tx['ts'] or 0).strftime('%H:%M')}
|
|
||||||
|
|
||||||
return response.html(tpl.render(items=items, message=message, last_tx=last_tx, tx_info=tx_info))
|
|
||||||
|
|
||||||
@bp.get("/poll_barcode")
|
|
||||||
async def give_barcode(request):
|
|
||||||
|
|
||||||
request.app.ctx.nfc_reads[request.ip] = Future()
|
|
||||||
|
|
||||||
try:
|
|
||||||
bcd = await asyncio.wait_for(request.app.ctx.nfc_reads[request.ip], 20)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
if not request.ip in request.app.ctx.nfc_reads:
|
|
||||||
del request.app.ctx.nfc_reads[request.ip]
|
|
||||||
return response.json({'error': 'no_barcode'})
|
|
||||||
|
|
||||||
info = request.app.ctx.money.execute("SELECT count(*), coalesce(sum(amount), 0) FROM tx WHERE coalesce(is_canceled, 0) != 1 AND tag_id = ?", (bcd['id'],)).fetchone()
|
|
||||||
|
|
||||||
order = await request.app.ctx.om.get_order(nfc_id=bcd['id'])
|
|
||||||
|
|
||||||
desc = ("⚠️" if not bcd['is_secure'] else '') + (f"👤 {order.code} {order.name}" if order else f"🪙 {bcd['id']}") + f" · Transazioni: {info[0]}"
|
|
||||||
|
|
||||||
return response.json({**bcd, 'txamt': info[0], 'balance': info[1], 'desc': desc})
|
|
72
nfc.py
72
nfc.py
|
@ -1,72 +0,0 @@
|
||||||
from sanic import Blueprint, exceptions, response
|
|
||||||
from random import choice
|
|
||||||
from ext import *
|
|
||||||
from config import headers, PROPIC_DEADLINE
|
|
||||||
from PIL import Image
|
|
||||||
from os.path import isfile
|
|
||||||
from os import unlink
|
|
||||||
from io import BytesIO
|
|
||||||
from hashlib import sha224
|
|
||||||
from time import time
|
|
||||||
from urllib.parse import unquote
|
|
||||||
from base64 import b16decode
|
|
||||||
from asyncio import Future
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
bp = Blueprint("nfc")
|
|
||||||
|
|
||||||
@bp.post("/nfc/read")
|
|
||||||
async def handle_reading(request):
|
|
||||||
payload = request.json
|
|
||||||
if payload['count']:
|
|
||||||
request.app.ctx.nfc_counts.execute('INSERT INTO read_count(tag_id, ts, count) VALUES (?,?,?)', (payload['id'], int(time()), payload['count']))
|
|
||||||
request.app.ctx.nfc_counts.commit()
|
|
||||||
|
|
||||||
if payload['boopbox_id']:
|
|
||||||
await request.app.ctx.boopbox_queue.put(payload)
|
|
||||||
return response.text('ok')
|
|
||||||
|
|
||||||
if not request.ip in request.app.ctx.nfc_reads:
|
|
||||||
return response.text('wasted read')
|
|
||||||
|
|
||||||
try:
|
|
||||||
request.app.ctx.nfc_reads[request.ip].set_result(payload)
|
|
||||||
except asyncio.exceptions.InvalidStateError:
|
|
||||||
del request.app.ctx.nfc_reads[request.ip]
|
|
||||||
|
|
||||||
return response.text('ok')
|
|
||||||
|
|
||||||
@bp.get("/fz23/<tag_id:([bc][0-9A-F]{14}x[0-9A-F]{6})>")
|
|
||||||
async def handle_nfc_tag(request, tag_id):
|
|
||||||
|
|
||||||
tag = re.match('([bc])([0-9A-F]{14})x([0-9A-F]{6})', tag_id)
|
|
||||||
|
|
||||||
# Store the read count
|
|
||||||
read_count = int.from_bytes(b16decode(tag.group(3)))
|
|
||||||
request.app.ctx.nfc_counts.execute('INSERT INTO read_count(tag_id, ts, count, is_web) VALUES (?,?,?,1)', (tag.group(2), int(time()), read_count))
|
|
||||||
request.app.ctx.nfc_counts.commit()
|
|
||||||
|
|
||||||
# If it's a coin, just show the coin template
|
|
||||||
if tag.group(1) == 'c':
|
|
||||||
return response.redirect(f"coin/{tag.group(2)}")
|
|
||||||
|
|
||||||
# If it's a badge, look for the corresponding order
|
|
||||||
for o in request.app.ctx.om.cache.values():
|
|
||||||
if o.nfc_id == tag.group(2):
|
|
||||||
return response.redirect(o.code)
|
|
||||||
|
|
||||||
raise exceptions.NotFound("Unknown tag :(")
|
|
||||||
|
|
||||||
@bp.get("/fz23/coin/<tag_id>")
|
|
||||||
async def show_coin(request, tag_id):
|
|
||||||
balance = request.app.ctx.money.execute('SELECT sum(amount) FROM tx WHERE tag_id = ?', (tag_id,)).fetchone()[0]
|
|
||||||
|
|
||||||
tpl = request.app.ctx.tpl.get_template('coin.html')
|
|
||||||
return response.html(tpl.render(balance=balance))
|
|
||||||
|
|
||||||
@bp.get("/fz23/<code:[A-Z0-9]{5}>")
|
|
||||||
async def show_order(request, code):
|
|
||||||
return response.html(f"<h1>Badge</h1><p>{code}</p>")
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Furizon Webint
|
||||||
|
Furizon Webint is a powerful control panel designed to complement Pretix, providing management of various aspects related to the attendance of participants at furry conventions. Originally developed for Furizon Beyond (2023), this application is currently undergoing a rehaul to become more versatile and adaptable for use in any convention.
|
||||||
|
|
||||||
|
## How does it work?
|
||||||
|
The integration with Pretix is achieved by leveraging a simple nginx rule. When individuals place orders through Pretix, they usually receive a "magic" link that allows them to manage their order. Using a nginx rule, we redirect these requests to this backend. This process is seamless because the essential information needed for managing Pretix orders can still be accessed via a shorter URL without compromising any functionality.
|
||||||
|
|
||||||
|
## Why not a pretix plugin?
|
||||||
|
Developing plugins for Pretix was far too tedious, and Pretix didn't have the flexibility needed for this panel.
|
||||||
|
|
||||||
|
## What can it do?
|
||||||
|
- User badges management (allow attendees to upload pictures within the deadlines)
|
||||||
|
- Manage hotel rooms (attendees can create, join, delete rooms)
|
||||||
|
- Show a nosecount public page
|
||||||
|
- Data export
|
||||||
|
- Car pooling (let attendees post announcements and organize trips)
|
||||||
|
- Karaoke Queue management (apply to sing for the karaoke contest and manage the queue)
|
||||||
|
- Manage the events and present them via API for usage with he app
|
||||||
|
- Export an API to be used for the mobile app (no plans to open source that, sorry ☹️)
|
||||||
|
- Check-in management
|
||||||
|
|
||||||
|
## How to run it
|
||||||
|
1. Create a Python virtual environment (venv).
|
||||||
|
2. Install the required dependencies from the `requirements.txt` file.
|
||||||
|
3. Edit the `config.py` file with your specific data. You can use `config.example.py` as a template to guide you.
|
||||||
|
4. Set up an nginx rule to redirect requests for `/manage/` and `/[a-z0-9]+/[a-z0-9]+/order/[A-Z0-9]+/[a-z0-9]+/open/[a-z0-9]+/` to the Furizon Webint backend.
|
||||||
|
5. Run `app.py`. By default, the application will listen on `0.0.0.0:8188`.
|
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 18 MiB |
Binary file not shown.
Before Width: | Height: | Size: 4.2 MiB |
Binary file not shown.
Before Width: | Height: | Size: 2.4 MiB |
BIN
res/snd/01.mp3
BIN
res/snd/01.mp3
Binary file not shown.
BIN
res/snd/02.mp3
BIN
res/snd/02.mp3
Binary file not shown.
BIN
res/snd/03.mp3
BIN
res/snd/03.mp3
Binary file not shown.
BIN
res/snd/04.mp3
BIN
res/snd/04.mp3
Binary file not shown.
BIN
res/snd/05.mp3
BIN
res/snd/05.mp3
Binary file not shown.
BIN
res/snd/06.mp3
BIN
res/snd/06.mp3
Binary file not shown.
BIN
res/snd/07.mp3
BIN
res/snd/07.mp3
Binary file not shown.
BIN
res/snd/08.mp3
BIN
res/snd/08.mp3
Binary file not shown.
BIN
res/snd/09.mp3
BIN
res/snd/09.mp3
Binary file not shown.
BIN
res/snd/10.mp3
BIN
res/snd/10.mp3
Binary file not shown.
BIN
res/snd/11.mp3
BIN
res/snd/11.mp3
Binary file not shown.
BIN
res/snd/12.mp3
BIN
res/snd/12.mp3
Binary file not shown.
BIN
res/snd/13.mp3
BIN
res/snd/13.mp3
Binary file not shown.
BIN
res/snd/14.mp3
BIN
res/snd/14.mp3
Binary file not shown.
BIN
res/snd/15.mp3
BIN
res/snd/15.mp3
Binary file not shown.
BIN
res/snd/16.mp3
BIN
res/snd/16.mp3
Binary file not shown.
BIN
res/snd/17.mp3
BIN
res/snd/17.mp3
Binary file not shown.
BIN
res/snd/18.mp3
BIN
res/snd/18.mp3
Binary file not shown.
BIN
res/snd/19.mp3
BIN
res/snd/19.mp3
Binary file not shown.
BIN
res/snd/20.mp3
BIN
res/snd/20.mp3
Binary file not shown.
BIN
res/snd/21.mp3
BIN
res/snd/21.mp3
Binary file not shown.
BIN
res/snd/22.mp3
BIN
res/snd/22.mp3
Binary file not shown.
BIN
res/snd/23.mp3
BIN
res/snd/23.mp3
Binary file not shown.
BIN
res/snd/24.mp3
BIN
res/snd/24.mp3
Binary file not shown.
BIN
res/snd/25.mp3
BIN
res/snd/25.mp3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 24 KiB |
8
room.py
8
room.py
|
@ -279,8 +279,8 @@ async def confirm_room(request, order: Order, quotas: Quotas):
|
||||||
thing = {
|
thing = {
|
||||||
'order': order.code,
|
'order': order.code,
|
||||||
'addon_to': order.position_positionid,
|
'addon_to': order.position_positionid,
|
||||||
'item': 39,
|
'item': ITEM_IDS['room'],
|
||||||
'variation': ([None, 16, 17, 18, 19, 20])[len(room_members)]
|
'variation': ROOM_MAP[len(room_members)]
|
||||||
}
|
}
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
|
@ -294,8 +294,8 @@ async def confirm_room(request, order: Order, quotas: Quotas):
|
||||||
thing = {
|
thing = {
|
||||||
'order': rm.code,
|
'order': rm.code,
|
||||||
'addon_to': rm.position_positionid,
|
'addon_to': rm.position_positionid,
|
||||||
'item': 42,
|
'item': ITEM_IDS['room'],
|
||||||
'variation': ([None, 21, 22, 23, 24, 25])[len(room_members)]
|
'variation': ROOM_MAP[len(room_members)]
|
||||||
}
|
}
|
||||||
res = await client.post(join(base_url, "orderpositions/"), headers=headers, json=thing)
|
res = await client.post(join(base_url, "orderpositions/"), headers=headers, json=thing)
|
||||||
|
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<head>
|
|
||||||
|
|
||||||
<meta charset="utf8" />
|
|
||||||
<title>{{order.name}}</title>
|
|
||||||
<meta name="viewport" content="width=380rem" />
|
|
||||||
<style>
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Lato&display=swap');
|
|
||||||
* {border:0;margin:0;padding:0;}
|
|
||||||
|
|
||||||
body{background:#000;color:#ddd;font-family:'Lato';font-size:1.1rem;}
|
|
||||||
h1 {font-size: 3.5em;text-transform:uppercase}
|
|
||||||
#center {margin:1em auto;padding:0 1rem;max-width:30rem;text-align:center;}
|
|
||||||
nav > a {font-size:1.1em;display:block;background-color:#336;color:#fff;text-decoration:none;padding:0.5em;border-radius:3em;text-align:center;margin:1em;font-weight:bold;}
|
|
||||||
a > img {display:inline;height:1em;line-height:1em;vertical-align:middle;}
|
|
||||||
p {margin:0.6em 0;}
|
|
||||||
hr {border:0;height:12px;background:url('wiggly.png');background-repeat:no-repeat;background-position:center;margin:1.5em 0;}
|
|
||||||
a {color:#f0f;}
|
|
||||||
|
|
||||||
.bebas {font-family:'Bebas Neue';font-weight:900;background: linear-gradient(to right, #f32170, #ff6b08, #cf23cf, #eedd44);
|
|
||||||
-webkit-text-fill-color: transparent;-webkit-background-clip: text;transform:.3s all;}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="center">
|
|
||||||
<img src="https://reg.furizon.net/res/propic/{{order.propic or 'default.png'}}" style="width:100%;max-height:10em;max-width:10em;"/>
|
|
||||||
<hr />
|
|
||||||
<h2 class="bebas" style="font-size:4em;font-family:'Bebas Neue';">{{order.name}}</h2>
|
|
||||||
<p>Is an attendee of Furizon 2023 :D<br/><br />
|
|
||||||
<hr />
|
|
||||||
<nav>
|
|
||||||
<a style="background:#1086D7;" href="https://t.me/{{order.telegram_username}}">Telegram</a>
|
|
||||||
</nav>
|
|
||||||
<h3 class="bebas">Stay cool~<br/>Stay Furizon</h3>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,85 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="stylesheet" href="/res/boopbox.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="commands">
|
|
||||||
<a href="javascript:document.documentElement.requestFullscreen();">Fs</a>
|
|
||||||
<a href="javascript:document.getElementById('wolf').setAttribute('disabled', 'true');">Wolf hide</a>
|
|
||||||
<a href="javascript:document.getElementById('wolf').removeAttribute('disabled');">Wolf show</a>
|
|
||||||
<a href="javascript:document.getElementById('tiger').setAttribute('disabled', 'true');">Tiger hide</a>
|
|
||||||
<a href="javascript:document.getElementById('tiger').removeAttribute('disabled');">Tiger show</a>
|
|
||||||
<a href="javascript:document.getElementById('msgbox').removeAttribute('disabled');">Chatbox show</a>
|
|
||||||
<a href="javascript:document.getElementById('msgbox').setAttribute('disabled', 'true');">Chatbox hide</a>
|
|
||||||
<span id="debug">Debug command</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--<div id="error">
|
|
||||||
<p>An unrecoverable error has happened. Please call foxo.</p>
|
|
||||||
</div>-->
|
|
||||||
|
|
||||||
<!--<div id="border"></div>-->
|
|
||||||
<div id="main">
|
|
||||||
<img class="char bye" id="wolf" src="/res/wolf.png" disabled="true" />
|
|
||||||
<img class="char bye" id="tiger" src="/res/tiger.png" disabled="true" />
|
|
||||||
|
|
||||||
<div id="msgbox" class="bye" disabled><span id="msgcontent"></span><span id="touch" disabled>(touch the screen to continue <img src="/res/touch.svg" />)</span></div>
|
|
||||||
|
|
||||||
<div id="events" class="welcome-back">
|
|
||||||
<div id="eventsbox">
|
|
||||||
<h1 style="margin-top:7em;display:none;" id="nothing-happening">It looks like nothing is happening now~<br />_-¯¯-¯_<br />See you later :)</h1>
|
|
||||||
</div>
|
|
||||||
<h1>See more events with our app!</h1>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<video id="tv" autoplay muted playsinline controls="false" poster="/res/tv.jpg" class="welcome-back">
|
|
||||||
<!-- <source id="video-source" src="http://192.168.32.189:8000/tron.webm" type="video/mp4"> -->
|
|
||||||
</video>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var video = document.getElementById('tv');
|
|
||||||
var source = document.getElementById('video-source');
|
|
||||||
|
|
||||||
video.controls = false; // Ensure controls are hidden
|
|
||||||
|
|
||||||
video.addEventListener('error', function() {
|
|
||||||
video.style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
function playVideo() {
|
|
||||||
var cachebuster = Date.now(); // Generate a unique timestamp
|
|
||||||
var videoUrl = 'http://192.168.32.189:8000/tron.webm?cachebuster=' + cachebuster;
|
|
||||||
source.src = videoUrl;
|
|
||||||
|
|
||||||
video.load();
|
|
||||||
video.play().catch(function() {
|
|
||||||
fallbackImage.style.display = 'block'; // Show fallback image on playback failure
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
playVideo();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div id="column" class="welcome-back">
|
|
||||||
<h1>Useful Info</h1>
|
|
||||||
<h2>CALL SECURITY</h2>
|
|
||||||
<p>Dial (+39) 375 732 4734</p>
|
|
||||||
<hr />
|
|
||||||
<h2>Reception Open</h2>
|
|
||||||
<p>Every day 8:00~19:00</p>
|
|
||||||
<hr />
|
|
||||||
<h2>NFC issues?</h2>
|
|
||||||
<p>Search for Foxo (Badge ID 3) or write to @unfoxo on Telegram</p>
|
|
||||||
<hr />
|
|
||||||
<h2 id="clock"></h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="nfcstat" class="welcome-back" disabled>Dealer's Den <img src="/res/icons/nfc.svg" /></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="/res/boopbox.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,44 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>You found a coin!</title>
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
||||||
<style>
|
|
||||||
@import url(https://fonts.bunny.net/css?family=arbutus:400);
|
|
||||||
body {font-family:'arbutus', 'Comic Sans MS', sans-serif;background:url('/res/wood.jpg') #222 no-repeat;background-size:cover;color:#eee;text-align:center;}
|
|
||||||
|
|
||||||
strong {color:#de0;}
|
|
||||||
main {display:block;max-width:26em;margin:1em auto;}
|
|
||||||
video {display:block;width:100%;}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
<video autoplay muted oncanplay="preloadSecondVideo()" onended="loopVideo()">
|
|
||||||
<source src="/res/video/coin-intro.webm" type="video/webm">
|
|
||||||
<img src="/res/video/coin.webp" alt="Fallback Image">
|
|
||||||
</video>
|
|
||||||
{% if balance < 0 %}
|
|
||||||
<h1>You found a<br/><strong>{{balance}}FZ</strong><br />coin!</h1>
|
|
||||||
{% else %}
|
|
||||||
<h1>You found a coin!<br />(but sadly, it's already spent)</h1>
|
|
||||||
{% endif %}
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function preloadSecondVideo() {
|
|
||||||
var secondVideo = new Image(); // Create an image element to preload the second video
|
|
||||||
secondVideo.src = '/res/video/coin-loop.webm';
|
|
||||||
}
|
|
||||||
|
|
||||||
function loopVideo() {
|
|
||||||
var video = document.querySelector('video');
|
|
||||||
video.src = '/res/video/coin-loop.webm';
|
|
||||||
video.loop = true;
|
|
||||||
video.load();
|
|
||||||
video.play();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,97 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
{% block title %}Your Booking{% endblock %}
|
|
||||||
{% block main %}
|
|
||||||
<main class="container">
|
|
||||||
<header>
|
|
||||||
<h1><img src="{{order.ans('propic') or '/res/avatar.jpg'}}" class="propic"> {{order.ans('fursona_name')}}'s booking</h1>
|
|
||||||
</header>
|
|
||||||
{% if order.status() == 'pending' %}
|
|
||||||
<p class="notice">⚠️ Your order is still pending. You will not be able to reserve a room. <a href="#">Check your payment status</a></p>
|
|
||||||
{% endif %}
|
|
||||||
{% if not order.ans('propic') %}
|
|
||||||
<p class="notice">⚠️ You still haven't uploaded a profile pic. <a href="#">Upload one now</a></p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<section id="room_prompt">
|
|
||||||
<h3>Room Status:
|
|
||||||
{% if order.ans('room_confirmed') %}
|
|
||||||
{{order.ans('room_confirmed')}}
|
|
||||||
<span style="color: green;">room confirmed</span></h3>
|
|
||||||
<p>Your room is confirmed! Enjoy the convention :)</p>
|
|
||||||
{% elif order.ans('room_members') %}
|
|
||||||
<span style="color: yellow;">pending room</span></h3>
|
|
||||||
<p>You have a room, but it is not confirmed! Make sure all room owners have paid, then use the button below to reserve the room for two people.</p>
|
|
||||||
|
|
||||||
<h5>Your room</h5>
|
|
||||||
<div class="grid people">
|
|
||||||
{% set room = namespace(forbidden=false) %}
|
|
||||||
{% for person in roommate_orders %}
|
|
||||||
<div>
|
|
||||||
<img class="propic" src="https://picsum.photos/200" />
|
|
||||||
<h3>{{person.ans('fursona_name')}}</h3>
|
|
||||||
<p>{{person.ans('staff_title') if person.ans('staff_title') else ''}}{{' · Fursuiter' if person.ans('is_fursuiter') != 'No'}}</p>
|
|
||||||
<p>{{('<strong style="color:red;">UNPAID</strong>' if person.status() == 'pending')|safe}}</p>
|
|
||||||
</div>
|
|
||||||
{% if person.status() != 'paid' %}
|
|
||||||
{% set room.forbidden = True %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h5>Available rooms</h5>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>Single room</td>
|
|
||||||
<td>{{room_qty[1]}} available</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Double room</td>
|
|
||||||
<td>{{room_qty[2]}} available</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Triple room</td>
|
|
||||||
<td>{{room_qty[3]}} available</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Quadruple room</td>
|
|
||||||
<td>{{room_qty[4]}} available</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Quintuple room</td>
|
|
||||||
<td>{{room_qty[5]}} available</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{% if room.forbidden %}
|
|
||||||
<p>Since at least one of your roommates still have a pending reservation, you will not be able to reserve a room for now.</p>
|
|
||||||
<button disabled>Reserve a room for 3 people</button>
|
|
||||||
{% else %}
|
|
||||||
<button>Reserve a room for 3 people</button>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
<span style="color: red;">no room</span></h3>
|
|
||||||
<p>You currently don't have any room. If you don't create or join one within 42 days, 10 hours, 32 minutes, 13 seconds, we will assign you to a random one.</p>
|
|
||||||
<form>
|
|
||||||
<div class="grid">
|
|
||||||
<button>Create a room</button>
|
|
||||||
<button>Join another room</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
<!--<section id="room_prompt">
|
|
||||||
<h3>Room status: </h3>
|
|
||||||
<p>Good news</p>
|
|
||||||
<div class="grid">
|
|
||||||
<button>Create a room</button>
|
|
||||||
<button>Join another room</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
</main>-->
|
|
||||||
{% endblock %}
|
|
|
@ -29,12 +29,12 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% for o in orders.values() if (o.code == o.room_id and not o.room_confirmed and len(o.room_members) > 1) %}
|
||||||
|
{% if loop.first %}
|
||||||
<hr />
|
<hr />
|
||||||
<h1>Unconfirmed rooms</h1>
|
<h1>Unconfirmed rooms</h1>
|
||||||
<p>These unconfirmed rooms are still being organized and may be subject to change. These rooms may also have openings for additional roommates. If you are interested in sharing a room, you can use this page to find potential roommates</p>
|
<p>These unconfirmed rooms are still being organized and may be subject to change. These rooms may also have openings for additional roommates. If you are interested in sharing a room, you can use this page to find potential roommates</p>
|
||||||
{% for o in orders.values() %}
|
{% endif %}
|
||||||
{% if o.code == o.room_id and not o.room_confirmed and len(o.room_members) > 1 %}
|
|
||||||
<h3>{% if o.room_confirmed %}🔒{% endif %}{{o.room_name}}</h3>
|
<h3>{% if o.room_confirmed %}🔒{% endif %}{{o.room_name}}</h3>
|
||||||
<div class="grid people" style="padding-bottom:1em;">
|
<div class="grid people" style="padding-bottom:1em;">
|
||||||
{% for m in o.room_members %}
|
{% for m in o.room_members %}
|
||||||
|
@ -51,15 +51,16 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% if loop.last %}</div>{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
{% for person in orders.values() if (not person.room_id or len(person.room_members) == 1) and (not person.room_confirmed)%}
|
||||||
|
{% if loop.first %}
|
||||||
<hr />
|
<hr />
|
||||||
<h1>Roomless furs</h1>
|
<h1>Roomless furs</h1>
|
||||||
<p>These furs have not yet secured a room for the convention. If you see your name on this list, please make sure to secure a room before the deadline to avoid being placed in a random room. If you are looking for a roommate or have an open spot in your room, you can use this page to find and connect with other furries who are also looking for housing 🎲</p>
|
<p>These furs have not yet secured a room for the convention. If you see your name on this list, please make sure to secure a room before the deadline to avoid being placed in a random room. If you are looking for a roommate or have an open spot in your room, you can use this page to find and connect with other furries who are also looking for housing 🎲</p>
|
||||||
<div class="grid people" style="padding-bottom:1em;">
|
<div class="grid people" style="padding-bottom:1em;">
|
||||||
{% for person in orders.values() %}
|
{% endif %}
|
||||||
{% if (not person.room_id or len(person.room_members) == 1) and (not person.room_confirmed) %}
|
|
||||||
<div style="margin-bottom: 1em;">
|
<div style="margin-bottom: 1em;">
|
||||||
<div class="propic-container">
|
<div class="propic-container">
|
||||||
<img class="propic propic-{{person.sponsorship}}" src="/res/propic/{{person.ans('propic') or 'default.png'}}" />
|
<img class="propic propic-{{person.sponsorship}}" src="/res/propic/{{person.ans('propic') or 'default.png'}}" />
|
||||||
|
@ -67,8 +68,8 @@
|
||||||
</div>
|
</div>
|
||||||
<h5>{{person.ans('fursona_name')}}</h5>
|
<h5>{{person.ans('fursona_name')}}</h5>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% if loop.last %}</div>{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
294
tpl/pos.html
294
tpl/pos.html
|
@ -1,294 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Fz vending</title>
|
|
||||||
<style>
|
|
||||||
/* HTML5 display-role reset for older browsers */
|
|
||||||
article, aside, details, figcaption, figure,
|
|
||||||
footer, header, hgroup, menu, nav, section {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
ol, ul {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
blockquote, q {
|
|
||||||
quotes: none;
|
|
||||||
}
|
|
||||||
blockquote:before, blockquote:after,
|
|
||||||
q:before, q:after {
|
|
||||||
content: '';
|
|
||||||
content: none;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
border-spacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shake {
|
|
||||||
0%, 100% {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
25%, 75% {
|
|
||||||
transform: translateX(-10px);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translateX(10px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {margin:0;padding:0;background:#222;color:#eee;font-family:"Open Sans", monospace;}
|
|
||||||
|
|
||||||
header {padding-left:0.5em;height:4rem;line-height:4rem;background:#111;font-size:2.2rem;color:#fa0;}
|
|
||||||
header img {height:3.6rem;margin-right:0.3em;display:inline-block;vertical-align:middle;}
|
|
||||||
header strong {font-size:1.05em;color:#f00;vertical-align:baseline;}
|
|
||||||
aside {position:fixed;height:100%;overflow:auto;top:0;width:18em;right:0;background:#111;}
|
|
||||||
|
|
||||||
main {padding-right:18em;}
|
|
||||||
|
|
||||||
.shake {animation: shake 0.3s 1;}
|
|
||||||
|
|
||||||
#toolkit {position:fixed;bottom:0;right:0;width:18em;padding:0.5em;box-sizing:border-box;background:#000;text-align:center;}
|
|
||||||
#toolkit img {display:inline;height:3rem;margin:0.5rem 0.1rem;border:1px dashed rgba(255,255,255,0.2);padding:0.2em;}
|
|
||||||
|
|
||||||
h2 {margin-top:0;font-size: 2.2rem;line-height:4rem;display:block;text-align:center;background:#070;color:#eee;}
|
|
||||||
h3 {margin-top:1em;font-size: 1.4rem;display:block;text-align:center;padding:0.3em 0em;background:#fa0;color:#000;}
|
|
||||||
|
|
||||||
button {background: #fa0;padding:0.2em;margin:0.1em;font-size:1.2em;width:1.6em;border-radius:6px;border:none;transition:all 0.1s;}
|
|
||||||
button:active {transform: scale(0.95);}
|
|
||||||
|
|
||||||
aside strong {font-size:1.9em;color:#f00;vertical-align:baseline;}
|
|
||||||
aside button {font-size:3em;}
|
|
||||||
|
|
||||||
ul {padding:0.5em 1em;}
|
|
||||||
li {line-height:1.8em;}
|
|
||||||
p {margin-bottom: 1em;}
|
|
||||||
em {font-size: 1.3em;font-weight:bold;}
|
|
||||||
sup {font-size: 0.7em;color:#fc0;}
|
|
||||||
|
|
||||||
table {width:100%;font-size:1.3em;}
|
|
||||||
table td {padding:0.1em;}
|
|
||||||
table td:first-child {color:#0f0;text-align:right;}
|
|
||||||
|
|
||||||
#mask {position:fixed;height:100%;width:100%;top:0;left:0;background:rgba(0,0,0,0.3);z-index:9999;backdrop-filter: blur(20px);text-align:center;}
|
|
||||||
#mask h1 {font-size:4em;margin:3em 0 0.6em;}
|
|
||||||
#mask button {font-size:3em;background:#ddd;}
|
|
||||||
|
|
||||||
.secret {display:none;}
|
|
||||||
|
|
||||||
input {color:#eee;background:#000;font-size:2em;width:2em;font-size:center;border:none;text-align:center;}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header id="nfcInfo">{{message or 'Scannerizza un NFC...'}}</header>
|
|
||||||
<form method="post" action="pos">
|
|
||||||
<input type="hidden" id="totalValue" name="total" />
|
|
||||||
<input type="hidden" id="nfcid" name="nfc_id" value="" />
|
|
||||||
<main>
|
|
||||||
<table>
|
|
||||||
{% for item in items %}
|
|
||||||
<tr {% if item.price > 0 %}class="secret"{% endif %}>
|
|
||||||
<td>{{item.name}}</td>
|
|
||||||
<td><strong>{{item.price}}FZ</strong></td>
|
|
||||||
<td>
|
|
||||||
<button type="button" onclick="decrementValue(this)">-</button>
|
|
||||||
<input type="text" name="itm_{{item.id}}" value="0" data-price="{{item.price}}" autocomplete="off" oninput="updateTotal();" />
|
|
||||||
<button type="button" onclick="incrementValue(this)">+</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</main>
|
|
||||||
<aside>
|
|
||||||
<h2><span id="badgeBalance">0</span> FZ</h2>
|
|
||||||
<p style="text-align:center;">Totale <strong><span id="totalValueShow">0</span>FZ</strong></p>
|
|
||||||
<div style="text-align:center;"><button style="background:#090;" id="sendButton" disabled="disabled">✓</button><button type="button" onClick="zeroItems();" style="background:#c00;">⛌</button></div>
|
|
||||||
<h3 id="triggerElement">Ultime transazioni</h3>
|
|
||||||
{% for tx in last_tx %}
|
|
||||||
<ul>
|
|
||||||
<li><em>{{tx_info[tx['id']]['order'].name or tx['tag_id']}}<sup>{{tx_info[tx['id']]['time']}}</sup></em></li>
|
|
||||||
{% for tx_item in tx_info[tx['id']]['items'] %}
|
|
||||||
<li><strong>{{tx_item['qty']}}x</strong> {{tx_item['name']}}</li>
|
|
||||||
{% endfor %}
|
|
||||||
<li><em>Totale: {{tx['amount']}}FZ</em></li>
|
|
||||||
</ul>
|
|
||||||
{% endfor %}
|
|
||||||
<span style="display:none;" id="debug"></span>
|
|
||||||
</aside>
|
|
||||||
<div id="toolkit">
|
|
||||||
<a href="pos"><img src="/res/icons/refresh.svg" style="filter: invert(1);"/></a>
|
|
||||||
<a href="#"><img src="/res/icons/qr.svg" id="executeButton" style="filter: invert(1);"/></a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
let isClosing = false;
|
|
||||||
|
|
||||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
||||||
function handleBeforeUnload(event) {
|
|
||||||
isClosing = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const triggerElement = document.getElementById('triggerElement');
|
|
||||||
const secretElements = document.querySelectorAll('.secret');
|
|
||||||
let secretElementsVisible = localStorage.getItem('secretElementsVisible');
|
|
||||||
|
|
||||||
let pressStart;
|
|
||||||
let isPress = false;
|
|
||||||
|
|
||||||
triggerElement.addEventListener('mousedown', handleMouseDown);
|
|
||||||
triggerElement.addEventListener('touchstart', handleMouseDown);
|
|
||||||
triggerElement.addEventListener('mouseup', handleMouseUp);
|
|
||||||
triggerElement.addEventListener('touchend', handleMouseUp);
|
|
||||||
|
|
||||||
function handleMouseDown() {
|
|
||||||
pressStart = Date.now();
|
|
||||||
isPress = true;
|
|
||||||
setTimeout(toggleSecretElements, 1200); // Delay the toggle action by 500 milliseconds
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMouseUp() {
|
|
||||||
isPress = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (secretElementsVisible === 'true') {
|
|
||||||
secretElements.forEach(element => {
|
|
||||||
element.classList.remove('secret');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const executeButton = document.getElementById('executeButton');
|
|
||||||
|
|
||||||
executeButton.addEventListener('click', () => {
|
|
||||||
const code = prompt('Enter the code:');
|
|
||||||
if (code && /^([A-F0-9]{2}){4,}$/.test(code)) {
|
|
||||||
const payload = {
|
|
||||||
id: code,
|
|
||||||
is_secure: false,
|
|
||||||
count: null,
|
|
||||||
boopbox_id: null
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch('/nfc/read', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
// Handle the response data as needed
|
|
||||||
console.log(data);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
// Handle any errors that occur during the POST request
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
} else if (code !== null) {
|
|
||||||
alert('Invalid code format. Please enter a valid code.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function toggleSecretElements() {
|
|
||||||
const pressDuration = Date.now() - pressStart;
|
|
||||||
if (pressDuration >= 1000 && isPress) {
|
|
||||||
secretElementsVisible = !secretElementsVisible;
|
|
||||||
|
|
||||||
if (secretElementsVisible) {
|
|
||||||
localStorage.setItem('secretElementsVisible', 'true');
|
|
||||||
secretElements.forEach(element => {
|
|
||||||
element.classList.remove('secret');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('secretElementsVisible');
|
|
||||||
secretElements.forEach(element => {
|
|
||||||
element.classList.add('secret');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to perform long-polling
|
|
||||||
function longPoll() {
|
|
||||||
fetch('poll_barcode')
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
// Update the table with received JSON data
|
|
||||||
|
|
||||||
if(!data.error) {
|
|
||||||
document.getElementById('nfcInfo').classList.remove('shake');
|
|
||||||
document.getElementById('nfcInfo').innerHTML = data.desc;
|
|
||||||
void document.getElementById('nfcInfo').offsetWidth;
|
|
||||||
document.getElementById('nfcInfo').classList.add('shake');
|
|
||||||
|
|
||||||
document.getElementById('nfcid').value = data.id;
|
|
||||||
document.getElementById('badgeBalance').innerHTML = data.balance;
|
|
||||||
document.getElementById('debug').innerHTML = JSON.stringify(data);
|
|
||||||
} else {
|
|
||||||
console.log("Got no barcode.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the next long-poll request
|
|
||||||
updateTotal();
|
|
||||||
longPoll();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
if(isClosing) return;
|
|
||||||
document.getElementById('nfcInfo').innerHTML = error;
|
|
||||||
// Retry long-polling after a delay in case of errors
|
|
||||||
setTimeout(longPoll, 1000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start long-polling
|
|
||||||
longPoll();
|
|
||||||
|
|
||||||
|
|
||||||
function incrementValue(button) {
|
|
||||||
var input = button.previousElementSibling;
|
|
||||||
var value = parseInt(input.value) || 0;
|
|
||||||
input.value = Math.max(value + 1, 0);
|
|
||||||
updateTotal();
|
|
||||||
}
|
|
||||||
|
|
||||||
function decrementValue(button) {
|
|
||||||
var input = button.nextElementSibling;
|
|
||||||
var value = parseInt(input.value) || 0;
|
|
||||||
input.value = Math.max(value - 1, 0);
|
|
||||||
updateTotal();
|
|
||||||
}
|
|
||||||
|
|
||||||
function zeroItems() {
|
|
||||||
var inputs = document.querySelectorAll('input[name^="itm_"]');
|
|
||||||
inputs.forEach(function(input) {
|
|
||||||
input.value = 0;
|
|
||||||
});
|
|
||||||
updateTotal();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTotal() {
|
|
||||||
var inputs = document.querySelectorAll('input[name^="itm_"]');
|
|
||||||
var total = 0;
|
|
||||||
inputs.forEach(function(input) {
|
|
||||||
var value = parseInt(input.value) || 0;
|
|
||||||
var price = parseFloat(input.getAttribute('data-price'));
|
|
||||||
total += value * price;
|
|
||||||
});
|
|
||||||
document.getElementById('totalValue').value = total;
|
|
||||||
document.getElementById('totalValueShow').innerHTML = total;
|
|
||||||
|
|
||||||
let badgeBalance = Number(document.getElementById('badgeBalance').innerHTML);
|
|
||||||
|
|
||||||
if(badgeBalance+total < 0 || total == 0) {
|
|
||||||
document.getElementById('sendButton').setAttribute("disabled", "disabled");
|
|
||||||
} else {
|
|
||||||
document.getElementById('sendButton').removeAttribute("disabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</html>
|
|
Loading…
Reference in New Issue