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.
|
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