Alpha release
This commit is contained in:
parent
62681acd16
commit
eeda5062ae
14 changed files with 842 additions and 1 deletions
13
Options.ini
Normal file
13
Options.ini
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[DEFAULT]
|
||||
host = 'http://0.0.0.0:5000'
|
||||
max_desks = 8
|
||||
|
||||
[SETTINGS]
|
||||
lockqrcode_whit_secret = True
|
||||
user = Admin
|
||||
path_json_settings = '/Server/json/availiable-products.json'
|
||||
|
||||
[OTHER]
|
||||
log_diagnose = True
|
||||
first_startup = False
|
||||
|
||||
39
README.md
39
README.md
|
|
@ -1,2 +1,39 @@
|
|||
# table-order
|
||||
# table-order (Alpha)
|
||||
|
||||
# Description
|
||||
Table ordering with a QR code allows customers to scan a code at their table, access a digital menu, and place their order directly from their smartphone, streamlining the ordering process totaly writen in Python.
|
||||
|
||||
Keep in mind there are currently bugs and also missing features all listed below please don't use in production jet!
|
||||
# How to set it up
|
||||
---
|
||||
## Sites
|
||||
currently there are 4 Sites
|
||||
|
||||
/login -> to Login
|
||||
/logout -> just browse to this URL ending to logout for now
|
||||
/orders -> All open orders (Not automatically refreshign on new order)
|
||||
/order -> Where users can place there order.
|
||||
to change what you can order take a look at the example in /json/availiable-products.json
|
||||
|
||||
## Settings
|
||||
All settings are in /Options.ini
|
||||
Default has to be changed to your needs:
|
||||
|
||||
host -> is where the QR code points to whit secret if enabled.
|
||||
max_desks -> How many desk it shoud create.
|
||||
Execute in Terminal the "pip install -r required.txt" to download depending and "python app.py" to run the Flask server and whit it the webserver.
|
||||
|
||||
##
|
||||
|
||||
Note:
|
||||
This are early stages of this project listed are missing and planed.
|
||||
# To Does
|
||||
---
|
||||
Must have features / fixes
|
||||
TODO's in prio sortet
|
||||
1. #TODO on new orderGet refresh orders list
|
||||
2. #BUG Fixing
|
||||
3. #Feature STOCK management
|
||||
4. #TODO Settings and init menu or buttons
|
||||
5. #Polish Make the HTML prettier for all
|
||||
6. #Polish LOG exacter
|
||||
|
|
|
|||
77
Server/DB/handler.py
Normal file
77
Server/DB/handler.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import datetime
|
||||
import os
|
||||
|
||||
from typing import List
|
||||
|
||||
from sqlalchemy import create_engine, BLOB, Column, Boolean, Float, Integer, String, JSON, DateTime, ForeignKey
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import DeclarativeBase, sessionmaker, relationship, Mapped, mapped_column
|
||||
from loguru import logger
|
||||
|
||||
from flask_login import UserMixin
|
||||
|
||||
# create an in-memory SQLite database
|
||||
engine = create_engine('sqlite:///db.sqlite', echo=True)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class User(UserMixin, Base):
|
||||
__tablename__ = 'User'
|
||||
username = Column(String, primary_key=True, unique=True)
|
||||
hashed_password = Column(String)
|
||||
salt = Column(String)
|
||||
class Log(Base): #TODO uSe
|
||||
__tablename__ = 'Log'
|
||||
number = Column(Integer, primary_key=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.now)
|
||||
desk_ = Column(Integer)
|
||||
ip = Column(Integer)
|
||||
def __init__(self, desk, ip, tel_nr):
|
||||
self.desk = desk
|
||||
self.ip = ip
|
||||
|
||||
class QR(Base): #TODO: Decide if encryption Salting hasing and anti rainbow attack is eeded for the qr codes?
|
||||
__tablename__ = 'qr'
|
||||
desk = Column(Integer, primary_key=True)
|
||||
qr_secret = Column(Integer)
|
||||
qr_img = Column(BLOB)
|
||||
|
||||
def __init__(self, desk, qr_img, qr_secret=0):
|
||||
self.desk = desk
|
||||
self.qr_secret = qr_secret
|
||||
self.qr_img = qr_img
|
||||
|
||||
class Product(Base):
|
||||
__tablename__ = 'product'
|
||||
id = mapped_column(Integer, primary_key=True)
|
||||
name = Column(String)
|
||||
quantity = Column(Integer)
|
||||
order_id = mapped_column(ForeignKey("order.id"))
|
||||
order = relationship("Order", back_populates="order")
|
||||
|
||||
def __init__(self, quantity,name, order_id):
|
||||
self.quantity = quantity
|
||||
self.name = name
|
||||
self.order_id = order_id
|
||||
self.date = datetime.datetime.now()
|
||||
|
||||
class Order(Base):
|
||||
__tablename__ = 'order'
|
||||
id = mapped_column(Integer, primary_key=True)
|
||||
order = relationship("Product", back_populates="order")
|
||||
desk_number = Column(Integer)
|
||||
finished = Column(Boolean)
|
||||
|
||||
def __init__(self, desk_number, finished=False, ):
|
||||
self.desk_number = desk_number
|
||||
self.desk = 1
|
||||
#TODO: replace product-order.json whit json object POST from flask
|
||||
# That means a UI for setting all values.
|
||||
self.finished = finished
|
||||
|
||||
Base.metadata.create_all(engine)
|
||||
147
Server/DB/querys.py
Normal file
147
Server/DB/querys.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
|
||||
from loguru import logger
|
||||
import json
|
||||
import bcrypt
|
||||
|
||||
from Server.DB.handler import QR, Product, Order, session, User
|
||||
|
||||
class compare:
|
||||
def is_user_pass_valid(username, password):
|
||||
session_username = session.query(User).filter(User.username == username).one()
|
||||
session.commit()
|
||||
print(session_username.hashed_password)
|
||||
if bcrypt.hashpw(password.encode('utf-8'), session_username.salt) == session_username.hashed_password.encode('utf-8'):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_QRSecret_valid(desk, secret):#TODO FIX! #Feature = True #TODO:Encryption Salting hasing and anti rainbow attack
|
||||
q = session.query(QR).\
|
||||
filter(QR.desk.like(int(desk))).\
|
||||
order_by(QR.qr_secret)
|
||||
q_QR = q.with_session(session).one()
|
||||
if q_QR.qr_secret == secret:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def process_main(ordered_list, desk):
|
||||
'''
|
||||
Get all verified Products from the order to the db entry
|
||||
|
||||
Input: Array ordered_list, INT desk
|
||||
|
||||
OUT: True, False if succeded adding to db
|
||||
'''
|
||||
|
||||
if compare.verifie_order(ordered_list):
|
||||
#products = Product()
|
||||
order = Order(desk)
|
||||
session.add(order)
|
||||
session.commit()
|
||||
for product in ordered_list:
|
||||
if int(product[0]) > 0: #dont add not ordered to list
|
||||
productDB = Product(quantity=int(product[0]),name=product[1],order_id = order.id)
|
||||
|
||||
session.add(productDB)
|
||||
session.commit()
|
||||
logger.success("Order at desk "+ str(desk) +" resived")
|
||||
# add a new ordered_list to the database
|
||||
return True
|
||||
|
||||
else:
|
||||
print("There was an invalid order! Someone messing whit source?")
|
||||
return False
|
||||
#TODO: Placeholder
|
||||
|
||||
def verifie_order(ordered_list):
|
||||
'''
|
||||
if self.desk > self.desk_numbers:
|
||||
return False
|
||||
'''
|
||||
valid_products_list = get.valid_products()
|
||||
|
||||
for product in ordered_list:
|
||||
print(product[1],' ', valid_products_list)
|
||||
if product[1] in valid_products_list: #product[0] is quantiiy 1 is name_product
|
||||
pass
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
class add:
|
||||
def _create_user(username, password):
|
||||
if session.query(User).filter_by(username = username).count() < 1:
|
||||
bytes = password.encode('utf-8')
|
||||
salt = bcrypt.gensalt()
|
||||
hash = bcrypt.hashpw(bytes, salt).decode('utf-8')
|
||||
user = User(username = username, hashed_password = hash, salt= salt)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
|
||||
|
||||
def _order(orders, desk):
|
||||
'''
|
||||
add order to db
|
||||
|
||||
INPUT: DICT orders, desk
|
||||
OUT: True succes False failure
|
||||
'''
|
||||
pass
|
||||
|
||||
class get:
|
||||
'''
|
||||
def user_in_User(username):
|
||||
|
||||
calls db if user in User table
|
||||
|
||||
q_session = session.query(User).filter(User.username == username).one()
|
||||
session.commit()
|
||||
if q_session:
|
||||
return q_session.id
|
||||
return False
|
||||
'''
|
||||
def valid_products(get_json_=False):
|
||||
with open(config['SETTINGS']['path_json_settings'], 'r') as file:
|
||||
#Parse Json Product List
|
||||
products = json.load(file)
|
||||
if get_json_:
|
||||
return products
|
||||
else:
|
||||
valide_products = []
|
||||
for category in products.get('products'):
|
||||
for product in products.get('products').get(category):
|
||||
print(product)
|
||||
if int(product['quantity']) >= 0:
|
||||
valide_products.append(product['name'])
|
||||
return valide_products
|
||||
|
||||
def all_orders():
|
||||
# All unfinished orders (finished is False or string "False")
|
||||
result = []
|
||||
desk = 1
|
||||
orders = session.query(Order).filter(Order.finished == 0).all()
|
||||
session.commit()
|
||||
for order in orders:
|
||||
products = session.query(Product).filter(Product.order_id == order.id , Order.desk_number == desk).all()
|
||||
session.commit()
|
||||
list_products = []
|
||||
for p in products:
|
||||
#What it shows in orders.html
|
||||
list_products.append(" x" + str(p.quantity) +" "+ p.name )
|
||||
result.append((order.desk_number, list_products, order.id))
|
||||
return result
|
||||
class update:
|
||||
#TODO GET JSON to -> DB
|
||||
def update_products_fromJSON(self):
|
||||
pass
|
||||
class set:
|
||||
def finish_order(order_number):
|
||||
order = session.query(Order).filter(Order.id == int(order_number)).first()
|
||||
if order:
|
||||
order.finished = 1 # or "True
|
||||
session.commit()
|
||||
logger.info("Order "+str(order_number)+" marked as finished.")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
151
Server/Host/flaskApp.py
Normal file
151
Server/Host/flaskApp.py
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
|
||||
from Server.DB.handler import Order, User
|
||||
from flask import Flask, abort,flash, render_template, request, redirect, url_for, jsonify
|
||||
# create a session to manage the connection to the database
|
||||
import time
|
||||
import configparser
|
||||
|
||||
|
||||
from Server.DB.handler import session, QR
|
||||
from Server.DB.querys import compare, get, set, add
|
||||
|
||||
from flask_login import LoginManager, login_required, login_user, logout_user
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from wtforms import StringField, PasswordField, SubmitField, BooleanField
|
||||
from Tools.qrGenerator import generate_QR_Code
|
||||
import os
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.sections()
|
||||
config.read('Options.ini')
|
||||
|
||||
login_manager = LoginManager()
|
||||
|
||||
csrf = CSRFProtect()
|
||||
|
||||
app = Flask(__name__)
|
||||
SECRET_KEY = os.urandom(32)
|
||||
app.config['SECRET_KEY'] = SECRET_KEY
|
||||
csrf.init_app(app)
|
||||
|
||||
|
||||
login_manager.init_app(app)
|
||||
|
||||
#default db values
|
||||
# create the Base if not present whit default values like admin+password and qr codes.
|
||||
|
||||
if config['OTHER']['first_startup'] == 'True':
|
||||
print("Please enter a Secure Admin Password:")
|
||||
add._create_user('Admin',input())
|
||||
config.set('OTHER', 'first_startup', 'False')
|
||||
with open('Options.ini', 'w') as configfile:
|
||||
config.write(configfile)
|
||||
generate_QR_Code()
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def user_loader(username):
|
||||
user = User()
|
||||
user.id = username
|
||||
print(username, user.id)
|
||||
return user
|
||||
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
username = StringField('name')
|
||||
password = PasswordField('Password')
|
||||
remember_me = BooleanField('Remember Me') #TODO
|
||||
submit = SubmitField('Submit')
|
||||
|
||||
|
||||
@app.route("/",)
|
||||
def view_form():
|
||||
return "loaded"
|
||||
|
||||
@app.route("/login", methods=['GET','POST'])
|
||||
def login():
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = user_loader(form.username.data)
|
||||
#TODO if username not in User:
|
||||
#TODO return if username or pass wrong a red textbox in ui...
|
||||
if user and compare.is_user_pass_valid(form.username.data, form.password.data):
|
||||
login_user(user) # store user id in session
|
||||
|
||||
#TODO url_has_allowed_host_and_scheme should check if the url is safe
|
||||
# for redirects, meaning it matches the request host.
|
||||
# See Django's url_has_allowed_host_and_scheme for an example.
|
||||
# if not url_has_allowed_host_and_scheme(next, request.host):
|
||||
# return abort(400)
|
||||
#next = request.args.get('next') is written in documentaion FLASK
|
||||
#return redirect(next, url_for('orders')) # redirect to orders page
|
||||
return redirect(url_for('orders')) # redirect to orders page
|
||||
return render_template('login.html', form=form)
|
||||
|
||||
@app.route("/logout") #TODO make a button
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect('/')
|
||||
|
||||
@app.route("/orders", methods=['GET','POST'])
|
||||
@login_required
|
||||
def orders(): #TODO on new orderGet refresh orders list
|
||||
if request.method == 'POST':
|
||||
order_id = request.form.get('order_id')
|
||||
action = request.form.get('action') # 'finish' or 'undo'
|
||||
|
||||
order = session.get(Order, int(order_id))
|
||||
if order: #SQL error handeling just in case
|
||||
if action == 'finish':
|
||||
order.finished = True
|
||||
elif action == 'undo':
|
||||
order.finished = False
|
||||
session.commit()
|
||||
return jsonify(success=True)
|
||||
return jsonify(success=False)
|
||||
|
||||
return render_template('orders.html', orders=get.all_orders())
|
||||
|
||||
@app.route("/order_get", methods=['GET','POST'])
|
||||
def order_get():
|
||||
desk= 1
|
||||
|
||||
#POST order
|
||||
if request.method == 'POST' and config['SETTINGS']['lockqrcode_whit_secret'] == True:
|
||||
ordered_list = []
|
||||
desk = request.form['desk']
|
||||
form = request.form
|
||||
for key in form:
|
||||
if key.startswith('order-name.'):
|
||||
name = key.partition('.')[-1]
|
||||
value = request.form[key]
|
||||
ordered_list.append([value,name])
|
||||
|
||||
# Adding order to DB
|
||||
if compare.process_main(ordered_list, desk):
|
||||
return '<h1>your order got ressived!</h1>'
|
||||
return '<h1>your order coudnt be ressived try again Error:SQL </h1>'
|
||||
|
||||
# Veriefie if auth or not from GET
|
||||
elif request.method == 'GET':
|
||||
desk = int(request.args['desk'])
|
||||
secret = int(request.args['secret'])
|
||||
try:#TODO FIX! #Feature = True #TODO:Encryption Salting hasing and anti rainbow attack for qr code?? (needed?)
|
||||
if config['SETTINGS']['lockqrcode_whit_secret'] and compare.is_QRSecret_valid(desk,secret):
|
||||
return render_template('index.html', desk=desk, MAX_DESKS=config['DEFAULT']['max_desks'], orderableItems = get.valid_products(get_json_=True))
|
||||
elif config['SETTINGS']['lockqrcode_whit_secret'] == False:
|
||||
return render_template('index.html', desk=desk, MAX_DESKS=config['DEFAULT']['max_desks'],orderableItems = get.valid_products(get_json_=True))
|
||||
return '<h1>404 wrong Secret?</h1>' #TODO Make the HTML prettier for all
|
||||
except:
|
||||
return '<h1>Server/code issue?</h1>'
|
||||
elif config['SETTINGS']['lockqrcode_whit_secret'] == True:
|
||||
return '<h1>your LOCKQRCODE is invalid.</h1>'
|
||||
else:
|
||||
return render_template('index.html',desk=desk, MAX_DESKS=config['DEFAULT']['max_desks'],orderableItems = get.valid_products(get_json_=True))
|
||||
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.db'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
7
Server/Host/static/order-sytle.css
Normal file
7
Server/Host/static/order-sytle.css
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.container {
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.container div {
|
||||
background-color: aquamarine;
|
||||
}
|
||||
79
Server/Host/static/style.css
Normal file
79
Server/Host/static/style.css
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
html body {
|
||||
min-height: 100%;
|
||||
list-style-type: none;
|
||||
}
|
||||
body {
|
||||
height: 98vh;
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* FLEXBOX DEFINITION */
|
||||
.flex-container {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.box {
|
||||
margin: 5px;
|
||||
line-height: 2vh;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 3em;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
/* BOX 1 DESK NR */
|
||||
|
||||
.desk-nr {
|
||||
margin-top: 5vw;
|
||||
background: lightcyan;
|
||||
color: black;
|
||||
font-size: 7vw;
|
||||
|
||||
}
|
||||
|
||||
.desk-nr div {
|
||||
padding-top: 5vw;
|
||||
margin-bottom: 6vw;
|
||||
}
|
||||
|
||||
.desk-select {
|
||||
padding-top: 1vw;
|
||||
padding-bottom: 1vw;
|
||||
}
|
||||
|
||||
/* BOX 3 CONTENT ORDER */
|
||||
|
||||
.content {
|
||||
margin-top: 5vw;
|
||||
flex-grow: 1;
|
||||
background: lightcyan;
|
||||
border: rgb(46, 42, 42);
|
||||
color: black;
|
||||
font-size: 8vw;
|
||||
}
|
||||
.content div {
|
||||
margin-top:8vw;
|
||||
}
|
||||
.products {
|
||||
font-size: 4vw;
|
||||
}
|
||||
|
||||
.content div summary {
|
||||
color: rgb(46, 42, 42);
|
||||
line-height: 8vh;
|
||||
border-bottom: 2px double rgb(46, 42, 42) ;
|
||||
margin-bottom: 5vw;
|
||||
}
|
||||
/* BOX 4 CONTENT ORDER BUTTON SEND */
|
||||
.order {
|
||||
background-color: rgb(209, 45, 45);
|
||||
padding: 1vw;
|
||||
line-height: 8vh;
|
||||
}
|
||||
53
Server/Host/templates/index.html
Normal file
53
Server/Host/templates/index.html
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<form name="order" onsubmit="return validateForm()" action="{{ url_for('order_get') }}" method="POST" class="flex-container">
|
||||
<li class="box desk-nr">Tisch Nummer
|
||||
<div>
|
||||
<select name="desk" class="desk-select">
|
||||
{% for e in range(1, MAX_DESKS+1) %}
|
||||
|
||||
{% if desk == e %}
|
||||
<option selected>{{e}}</option>
|
||||
|
||||
{% else %}
|
||||
<option>{{e}}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</li>
|
||||
<li class="box content">Menu
|
||||
{% for category in orderableItems.get('products') %}
|
||||
|
||||
|
||||
<div>
|
||||
<details>
|
||||
<summary>{{ category }}</summary>
|
||||
|
||||
|
||||
{% for product in orderableItems.get('products').get(category)%}
|
||||
|
||||
<div class="products" id="{ product['name'] }">
|
||||
{{product['name']}}
|
||||
<input id="points" type="number" value=0 name="{{ "order-name." + product['name'] }}" step="1">
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
</details>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
</li>
|
||||
|
||||
<button type="submit" class="box order">Weiter</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
23
Server/Host/templates/login.html
Normal file
23
Server/Host/templates/login.html
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Login Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Login Page</h1>
|
||||
<form method="POST" action="{{ url_for('login') }}">
|
||||
{{ form.csrf_token }}
|
||||
{{ form.username.label }}
|
||||
{{ form.username }}
|
||||
<br>
|
||||
<br>
|
||||
{{ form.password.label }}
|
||||
{{ form.password }}
|
||||
<br>
|
||||
<p>{{ form.remember_me }} {{ form.remember_me.label }}</p>
|
||||
<br>
|
||||
{{ form.submit }}
|
||||
</form>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
125
Server/Host/templates/orders.html
Normal file
125
Server/Host/templates/orders.html
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Orders</title>
|
||||
<style>
|
||||
table { border-collapse: collapse; width: 80%; margin: 40px auto; }
|
||||
th, td { border: 1px solid #cccccc; padding: 12px 18px; }
|
||||
th { background: #f0f0f0; }
|
||||
.finish-btn {
|
||||
background-color: #4CAF50; /* Green */
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 7px 18px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.undo-btn {
|
||||
background-color: #f44336; /* Red */
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 7px 18px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.finish-btn:hover { background-color: #388e3c; }
|
||||
.undo-btn:hover { background-color: #d32f2f; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 style="text-align:center;">Current Orders</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Desk</th>
|
||||
<th>Products</th>
|
||||
<th>Action</th>
|
||||
<th>Order-Nr</th>
|
||||
</tr>
|
||||
{% for order in orders%}
|
||||
<tr id="order-{{ order[2] }}">
|
||||
<td>{{ order[0] }}</td>
|
||||
<td>
|
||||
<ul>
|
||||
{% for product in order[1] %}
|
||||
<li>{{ product }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
class="finish-btn"
|
||||
onclick="markFinished({{ order[2] }})"
|
||||
id="btn-{{ order[2] }}">
|
||||
Finished
|
||||
</button>
|
||||
</td>
|
||||
<td>order {{ order[2] }} </td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="3">No open orders.</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<script>
|
||||
// Track active timers for each order
|
||||
window.activeTimers = {};
|
||||
|
||||
function markFinished(orderId) {
|
||||
fetch('/orders', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `order_id=${orderId}&action=finish`
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const btn = document.getElementById(`btn-${orderId}`);
|
||||
btn.className = 'undo-btn';
|
||||
btn.onclick = () => undoFinished(orderId);
|
||||
|
||||
// Start countdown (10s)
|
||||
let secondsLeft = 10;
|
||||
btn.textContent = `Undo (${secondsLeft}s)`;
|
||||
|
||||
// Store timer reference
|
||||
window.activeTimers[orderId] = setInterval(() => {
|
||||
secondsLeft--;
|
||||
btn.textContent = `Undo (${secondsLeft}s)`;
|
||||
|
||||
// Auto-remove after 10s
|
||||
if (secondsLeft <= 0) {
|
||||
clearInterval(window.activeTimers[orderId]);
|
||||
document.getElementById(`order-${orderId}`).remove();
|
||||
delete window.activeTimers[orderId];
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function undoFinished(orderId) {
|
||||
// Clear the countdown timer
|
||||
if (window.activeTimers[orderId]) {
|
||||
clearInterval(window.activeTimers[orderId]);
|
||||
delete window.activeTimers[orderId];
|
||||
}
|
||||
|
||||
fetch('/orders', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `order_id=${orderId}&action=undo`
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Revert button to original state
|
||||
const btn = document.getElementById(`btn-${orderId}`);
|
||||
btn.className = 'finish-btn';
|
||||
btn.textContent = 'Finished';
|
||||
btn.onclick = () => markFinished(orderId);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
37
Server/json/availiable-products.json
Normal file
37
Server/json/availiable-products.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"products": {
|
||||
"Süsses": [
|
||||
{
|
||||
"name": "Haribos",
|
||||
"price": 4,
|
||||
"quantity": 2
|
||||
},
|
||||
{
|
||||
"name": "Skittles",
|
||||
"price": 4,
|
||||
"quantity": 2
|
||||
}
|
||||
],
|
||||
"Getränke": [
|
||||
{
|
||||
"name": "IceTea",
|
||||
"price": 4,
|
||||
"quantity": 2
|
||||
},
|
||||
{
|
||||
"name": "Fanta",
|
||||
"price": 4,
|
||||
"quantity": 5
|
||||
}
|
||||
],
|
||||
"Öm": [
|
||||
{
|
||||
"name": "Molecule Man",
|
||||
"price": 10,
|
||||
"quantity": 5
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
56
Tools/qrGenerator.py
Normal file
56
Tools/qrGenerator.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Importing library
|
||||
import qrcode
|
||||
import random
|
||||
import io
|
||||
|
||||
from Server.DB.handler import QR
|
||||
from Server.DB.handler import session
|
||||
|
||||
import configparser
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
config.sections()
|
||||
config.read('Options.ini')
|
||||
|
||||
def randint(min,max):
|
||||
a = random.randint(min,max)
|
||||
return a
|
||||
|
||||
def generate_QR_Code():
|
||||
if config['SETTINGS']['lockqrcode_whit_secret'] == 'True':
|
||||
for desk in range(int(config['DEFAULT']['max_desks'])):
|
||||
img_byte_arr = io.BytesIO()
|
||||
secret = random.randint(1,9999999)
|
||||
data = config['DEFAULT']['host'] + '/order_get?desk='+str(desk)+'&secret='+str(secret)
|
||||
qr_img = qrcode.make(data)
|
||||
qr_img.save(img_byte_arr, format='PNG')
|
||||
img_byte_arr = img_byte_arr.getvalue()
|
||||
qr = QR(desk,img_byte_arr, qr_secret=secret)
|
||||
existing_qr = session.query(QR).filter_by(desk=desk).first()
|
||||
if existing_qr:
|
||||
existing_qr.qr_img = img_byte_arr # Update existing row
|
||||
existing_qr.qr_secret = secret
|
||||
else:
|
||||
session.add(qr) # Insert new row
|
||||
session.commit()
|
||||
else: #no Optional features
|
||||
for desk in range(int(config['DEFAULT']['max_desks'])):
|
||||
img_byte_arr = io.BytesIO()
|
||||
data = config['DEFAULT']['host'] + '/order_get?desk='+str(desk)
|
||||
qr_img = qrcode.make(data)
|
||||
qr_img.save(img_byte_arr, format='PNG')
|
||||
img_byte_arr = img_byte_arr.getvalue()
|
||||
qr = QR(desk,img_byte_arr)
|
||||
existing_qr = session.query(QR).filter_by(desk=desk).first()
|
||||
if existing_qr:
|
||||
existing_qr.qr_img = img_byte_arr # Update existing row
|
||||
existing_qr.qr_secret = 0
|
||||
else:
|
||||
session.add(qr) # Insert new row
|
||||
session.commit()
|
||||
|
||||
|
||||
|
||||
# Encoding data using make() function
|
||||
|
||||
# Saving as an image file
|
||||
27
app.py
Normal file
27
app.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import sys
|
||||
|
||||
from loguru import logger
|
||||
from Server.Host.flaskApp import app
|
||||
from Server.DB.querys import add
|
||||
|
||||
import configparser
|
||||
config = configparser.ConfigParser()
|
||||
config.sections()
|
||||
config.read('Options.ini')
|
||||
'''
|
||||
INFO if doing Asynchronous, Thread-safe, Multiprocess-safe
|
||||
All sinks added to the logger are thread-safe by default.
|
||||
They are not multiprocess-safe,
|
||||
but you can enqueue the messages to ensure logs
|
||||
integrity. This same argument can also be used if
|
||||
you want async logging.
|
||||
'''
|
||||
@logger.catch
|
||||
def run(): #TODO: sys.stderr back to a file log??
|
||||
logger.add(sys.stderr, format="{time} {level} {message}", filter="startup", level="INFO")
|
||||
logger.add(sys.stderr, backtrace=True, diagnose=config['OTHER']['log_diagnose'])
|
||||
app.run(debug=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
#First startup is handled in DB/handler
|
||||
run()
|
||||
9
required.txt
Normal file
9
required.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
flask==3.0.3;
|
||||
flask-login==0.6.3;
|
||||
flask_wtf==1.2.2;
|
||||
WTForms==3.2.1;
|
||||
Flask-SQLAlchemy==3.1.1.;
|
||||
qrcode==8.0
|
||||
pillow==11.1.0
|
||||
loguru==0.7.3
|
||||
bcrypt==4.3.0;
|
||||
Loading…
Add table
Add a link
Reference in a new issue