diff options
author | Riley L. <riley@e926.de> | 2024-11-03 18:31:13 +0100 |
---|---|---|
committer | Riley L. <riley@e926.de> | 2024-11-03 18:31:13 +0100 |
commit | 4961908376db687a74ff4a6b134430f1a57ce57b (patch) | |
tree | 799616503edfa238f40b4f2d59d7e28ded4b1edf /abrechenbarkeit.lua | |
parent | 368af91f8f55ff6356eb591e0d61873a1501794d (diff) | |
download | abrechenbarkeit-4961908376db687a74ff4a6b134430f1a57ce57b.tar abrechenbarkeit-4961908376db687a74ff4a6b134430f1a57ce57b.tar.bz2 abrechenbarkeit-4961908376db687a74ff4a6b134430f1a57ce57b.tar.zst |
css and rebrad
Diffstat (limited to 'abrechenbarkeit.lua')
-rwxr-xr-x | abrechenbarkeit.lua | 400 |
1 files changed, 400 insertions, 0 deletions
diff --git a/abrechenbarkeit.lua b/abrechenbarkeit.lua new file mode 100755 index 0000000..dc3685b --- /dev/null +++ b/abrechenbarkeit.lua @@ -0,0 +1,400 @@ +#!/usr/bin/env luajit + +local function escape(s) + return s:gsub("<", "<"):gsub("<", "<") +end + +local function urldecode(s) + if s == nil then return nil end + return s:gsub("+", " "):gsub("%%20", " ") +end + +local function urlencode(s) + if s == nil then return nil end + return s:gsub(" ", "%%20") +end + +local function parse_query(q) + if q == nil then return {} end + local data = {} + for pair in string.gmatch(q, "([^&]+)") do + local flag = string.match(pair, "^([^=]+)$") + if flag ~= nil then + data[flag] = "1" + else + local key, value = string.match(pair, "^([^=]+)=([^=]*)$") + if key ~= nil and value ~= nil then + data[key] = urldecode(value) + end + end + end + return data +end + +local function load_config() + local log = io.open("config", "r") + if log == nil then return {} end + local config = {} + for l in log:lines("l") do + if l ~= "" and l[0] ~= "#" then + local key, value = string.match(l, "^([^=]+)=([^=]*)") + if key ~= nil and value ~= nil then + config[key] = value + end + end + end + return config +end + +local config = load_config() +local path = os.getenv("PATH_INFO") +local method = os.getenv("REQUEST_METHOD") +local query = parse_query(os.getenv("QUERY_STRING")) + +local stylesheet = io.open("a.css"):read("a") +-- local stylesheet = [[ +-- /* body { background-color: #161616; } +-- h1, h2, h3, h4, h5, h6, p, label, a { color: #e2e2e2; } */ +-- .amount-presets form { display: inline-block; width: 60px } +-- .amount-pos { color: green; } +-- .amount-neg { color: red; } +-- nav h2 { display: inline-block } +-- .notif { padding: 0.5em; margin: 0.5em; background-color: #ddd; } +-- .notif.error { background-color: #faa; } +-- .notif p { margin: 5px; } +-- form.box { border: 2px solid grey; padding: 0.5em; margin: 0.5em; display: inline-block; } +-- form h3 { margin: 5px; } +-- ]] + +local script = [[ + document.addEventListener("keypress", ev => { + if (!(document.activeElement instanceof HTMLInputElement)) { + if (ev.code.startsWith("Digit")) + document.forms.buy_product.product.value += ev.code.substring(5) + if (ev.code == "Enter") + document.forms.buy_product.submit() + } + }) +]] + +local function respond(status, title, body) + print(string.format("Status: %d", status)) + print("Content-Type: text/html") + print("") + print(string.format([[ + <!DOCTYPE html> + <html><head> + <title>%s</title> + <meta charset="utf-8" /> + <style>%s</style> + <script>%s</script> + %s + </head> + <body> + <nav> + <a class="logo" href="/"> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-receipt-cutoff" viewBox="0 0 16 16"> + <path d="M3 4.5a.5.5 0 0 1 .5-.5h6a.5.5 0 1 1 0 1h-6a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h6a.5.5 0 1 1 0 1h-6a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h6a.5.5 0 1 1 0 1h-6a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5M11.5 4a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z"/> + <path d="M2.354.646a.5.5 0 0 0-.801.13l-.5 1A.5.5 0 0 0 1 2v13H.5a.5.5 0 0 0 0 1h15a.5.5 0 0 0 0-1H15V2a.5.5 0 0 0-.053-.224l-.5-1a.5.5 0 0 0-.8-.13L13 1.293l-.646-.647a.5.5 0 0 0-.708 0L11 1.293l-.646-.647a.5.5 0 0 0-.708 0L9 1.293 8.354.646a.5.5 0 0 0-.708 0L7 1.293 6.354.646a.5.5 0 0 0-.708 0L5 1.293 4.354.646a.5.5 0 0 0-.708 0L3 1.293zm-.217 1.198.51.51a.5.5 0 0 0 .707 0L4 1.707l.646.647a.5.5 0 0 0 .708 0L6 1.707l.646.647a.5.5 0 0 0 .708 0L8 1.707l.646.647a.5.5 0 0 0 .708 0L10 1.707l.646.647a.5.5 0 0 0 .708 0L12 1.707l.646.647a.5.5 0 0 0 .708 0l.509-.51.137.274V15H2V2.118z"/> + </svg> + Abrechenbarkeit</a> + <a href="/?log">Log</a> + <a href="/?products">Products</a> + <a href="https://codeberg.org/metamuffin/strichliste">Source</a> + </nav> + ]], escape(title), stylesheet, script, config.head_extra or "")) + body() + print("</body></html>") +end + +local function respond_error(message) + respond(400, "Error", function() + print(string.format("<p>Error: %s</p>", escape(message))) + end) +end + +local function redirect(path) + print("Status: 307") + print(string.format("Location: %s", path)) + print() +end + +local function form_data() + return parse_query(io.read()) +end + +local function format_duration(t) + if t > 86400 then return string.format("%d day%s", t / 86400, math.floor(t / 86400) ~= 1 and "s" or "") end + if t > 3600 then return string.format("%d hour%s", t / 3600, math.floor(t / 3600) ~= 1 and "s" or "") end + if t > 60 then return string.format("%d minute%s", t / 60, math.floor(t / 60) ~= 1 and "s" or "") end + return string.format("%d seconds", t) +end + +local function read_log() + local log = io.open("log", "r") + if log == nil then + return function() return nil end + end + local lines = log:lines("l") + return function() + local l = lines() + if l == "" or l == nil then + return nil + end + local time, username, amount, comment = string.match(l, "(%d+),([%w_ -]+),(-?%d+),([%w_ -]*)") + return tonumber(time), username, tonumber(amount), comment + end +end + +local function read_products() + local log = io.open("products", "r") + if log == nil then + return function() return nil end + end + local lines = log:lines("l") + return function() + local l = lines() + if l == "" or l == nil then + return nil + end + local barcode, amount, name = string.match(l, "([%w_-]+),(-?%d+),([%w_ -]*)") + return barcode, tonumber(amount), name + end +end + +local function balances() + local users = {} + for _, username, amount, _ in read_log() do + users[username] = (users[username] or 0) + amount + end + return users +end + +local function last_txns() + local users = {} + for time, username, _, _ in read_log() do + users[username] = time + end + return users +end + +local function error_box(message) + return string.format([[<div class="notif error"><p>Error: %s</p></div>]], message) +end + +local function r_user_post(username) + local data = form_data() + local amount = nil + local comment = "" + if data.product then + for p_barcode, p_amount, p_name in read_products() do + if p_barcode == data.product then + amount = p_amount + comment = p_name + end + end + if amount == nil then + return error_box("unknown product") + end + else + amount = tonumber(data.amount) + comment = data.comment or "" + end + if amount == nil then + return error_box("amount invalid") + end + if comment:match("^[%w_ -]*$") == nil then + return error_box("comment invalid") + end + local log = io.open("log", "a+") + if log == nil then + return error_box("failed to open log") + end + local time = os.time() + log:write(string.format("%d,%s,%d,%s\n", time, username, amount, comment)) + log:flush() + log:close() + return string.format([[ + <div class="notif"><p>Transaction successful: <strong class="amount-%s">%.02f€</strong> (%s)</p></div> + <audio src="%s" autoplay></audio> + ]], + amount >= 0 and "pos" or "neg", amount / 100, + escape(comment), + config.transaction_sound or "" + ) +end + +local function r_user(username) + local notif = nil + if method == "POST" then + notif = r_user_post(username) + end + return respond(200, string.format("Abrechenbarheit: %s", username), function() + print(string.format("<h1>%s</h1>", username)) + local balance = balances()[username] + local last_txn = last_txns()[username] + local new_user = balance == nil + balance = balance or 0 + if notif then print(notif) end + if new_user then + print([[ + <div class="notif"><p><i>This user account does not exist yet. It will only be created after the first transaction.</i></p></div> + ]]) + else + print(string.format([[ + <p><span class="balance">Current balance:</span><span class="amount-%s balance-value">%.02f€</p> + ]], balance >= 0 and "pos" or "neg", balance / 100)) + print(string.format([[ + <p>Last transaction added %s ago. <a href="/%s?log">View user log</a> + ]], format_duration(os.time() - last_txn), username)) + end + print([[<div class="transactions container">]]) + print([[<div class="amount-presets box">]]) + for _, type in ipairs({ 1, -1 }) do + for _, amount in ipairs({ 50, 100, 150, 200, 500, 1000 }) do + print(string.format([[ + <form action="" method="POST"> + <input type="number" name="amount" id="amount" value="%d" hidden /> + <input type="text" name="comment" id="comment" value="" hidden /> + <input type="submit" value="%s%.02f€" class="amount-%s" /> + </form> + ]], amount * type, ({ [-1] = "-", [1] = "+" })[type], amount / 100, + ({ [-1] = "neg", [1] = "pos" })[type])) + end + end + print("</div>") + print([[ + <form class="transaction box" action="" method="POST"> + <h3>Create Transaction</h3> + <label for="amount">Amount: </label> + <input type="number" name="amount" id="amount" /> + <label for="comment">Comment: </label> + <input type="text" name="comment" id="comment" /> + <input type="submit" value="Update" /> + </form> + <form class="transaction box" action="" method="POST" id="buy_product"> + <h3>Buy Product</h3> + <label for="product">Product: </label> + <input type="text" name="product" id="product" /> + <input type="submit" value="Buy" /> + </form> + ]]) + print("</div>") + end) +end + +local function r_log(filter) + return respond(200, "Abrechnungen", function() + print("<table>") + print("<tr><th>Time</th><th>Username</th><th>Amount</th><th>Comment</th></tr>") + for time, username, amount, comment in read_log() do + if filter == nil or filter == username then + print(string.format([[ + <tr> + <td>%d (%s ago)</td> + <td>%s</td> + <td class="amount-%s">%.02f€</td> + <td>%s</td> + <td> + <form action="/%s" method="POST"> + <input type="number" name="amount" id="amount" value="%d" hidden /> + <input type="text" name="comment" id="comment" value="Revert %s" hidden /> + <input type="submit" value="Revert" /> + </form> + </td> + </tr> + ]], + time, format_duration(os.time() - time), + escape(username), + amount >= 0 and "pos" or "neg", amount / 100, + escape(comment), + escape(username), + -amount, + escape(comment) + )) + end + end + print("</table>") + end) +end + +local function r_index() + return respond(200, "Abrechenbarkeit", function() + print([[ + <form action="/" method="GET" class="creation"> + <h3>User Creation</h3> + <label for="username">Username: </label> + <input type="text" name="create_user" id="username" /> + <input type="submit" value="Continue" /> + </form> + ]]) + print("<ul>") + for username, balance in pairs(balances()) do + print(string.format([[ + <li><a href="/%s"><span class="name">%s</span> <span class="amount amount-%s">%.02f€</span></a></li> + ]], + urlencode(username), + escape(username), + balance >= 0 and "pos" or "neg", balance / 100 + )) + end + print("</ul>") + end) +end + +local function r_create_user() + local username = query.create_user + if username:match("^([%w_ -]+)$") == nil then + return respond_error("invalid username " .. username) + end + return redirect(string.format("/%s", urlencode(username))) +end + +local function r_products() + respond(200, "Abrechenbare Products", function() + print("<h1>Product List</h1>") + print("<table><tr><th>Name</th><th>Price</th><th>Barcode</th></tr>") + for barcode, price, name in read_products() do + print(string.format([[ + <tr><td>%s</td><td class="amount-%s">%.02f€</td><td>%s</td></tr> + ]], + name, + price >= 0 and "pos" or "neg", price / 100, + barcode + )) + end + print("</table>") + end) +end + +local function extract_username() + if path == nil then + return respond_error("no path") + end + local username = urldecode(path:sub(2)) + if username == nil or username:match("^([%w_ -]+)$") == nil then + return nil + end + return username +end + +if path == "/" then + if query.products then + return r_products() + elseif query.log then + return r_log() + elseif query.create_user then + return r_create_user() + else + return r_index() + end +else + local username = extract_username() + if username == nil then + return respond_error("username invalid") + elseif query.log then + return r_log(username) + else + return r_user(username) + end +end |