mirror of
https://codeberg.org/metamuffin/abrechenbarkeit.git
synced 2024-12-28 07:54:35 +00:00
great refactor and new transaction scheme
This commit is contained in:
parent
2d116f15bb
commit
28e9e15e42
4 changed files with 235 additions and 182 deletions
|
@ -17,18 +17,36 @@
|
|||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
]] --
|
||||
|
||||
-- replace german chars with a better %w that allows unicode
|
||||
local matchers = {
|
||||
time = "(%d+)",
|
||||
user = "([%w_@ -öäüÖÄÜßẞ]+)",
|
||||
amount = "(-?%d+)",
|
||||
comment = "([%w_ -öäüÖÄÜßẞ]*)",
|
||||
barcode = "([%w_-]*)",
|
||||
name = "([%w_ -öäüÖÄÜßẞ]*)",
|
||||
}
|
||||
local matchers_global = (function()
|
||||
local s = {}
|
||||
for k, v in pairs(matchers) do s[k] = ("^%s$"):format(v) end
|
||||
return s
|
||||
end)()
|
||||
|
||||
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", " ")
|
||||
local t, _ = s:gsub("+", " "):gsub("%%(%x%x)",
|
||||
function(cap) return string.char(tonumber(cap, 16)) end)
|
||||
return t
|
||||
end
|
||||
|
||||
local function urlencode(s)
|
||||
if s == nil then return nil end
|
||||
return s:gsub(" ", "%%20")
|
||||
return s:gsub("[^%w]",
|
||||
function(cap) return string.format("%02x", string.byte(cap, 1)) end)
|
||||
end
|
||||
|
||||
local function parse_query(q)
|
||||
|
@ -72,7 +90,7 @@ local function load_translations(langs)
|
|||
if l ~= "" then
|
||||
local key, value = string.match(l, "^([^=]+)=([^=]*)")
|
||||
if key ~= nil and value ~= nil then
|
||||
t[key] = value
|
||||
t["+" .. key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -93,18 +111,23 @@ local script = io.open("script.js"):read("a")
|
|||
local function format(template, params)
|
||||
params = params or {}
|
||||
if template == nil then return "NIL TEMPLATE" end
|
||||
return string.gsub(template, "{([%w_\\.!]+)}", function(n)
|
||||
local raw = n:sub(1, 1) ~= "!"
|
||||
if not raw then n = n:sub(2) end
|
||||
local s = format(params[n] or translations[n] or "NIL PARAM", params)
|
||||
return raw and s or escape(s)
|
||||
local s, _ = string.gsub(template, "{([%w\\+_\\.!]+)}", function(n)
|
||||
local esc = n:sub(1, 1) == "!"
|
||||
if esc then n = n:sub(2) end
|
||||
local s = params[n] or translations[n] or "NIL PARAM"
|
||||
if not esc then s = format(s, params) end
|
||||
return esc and escape(s) or s
|
||||
end)
|
||||
return s
|
||||
end
|
||||
|
||||
local function format_amount(amount, tag, classes)
|
||||
local s = string.format("%.02f%s", amount / 100, config.unit or "€")
|
||||
local s = string.format("%s%.02f%s", amount > 0 and "+" or "", amount / 100, config.unit or "€")
|
||||
if tag == nil then return s end
|
||||
return string.format([[<%s class="amount-%s %s">%s</%s>"]], tag, amount >= 0 and "pos" or "neg", classes or "", s, tag)
|
||||
return format(
|
||||
[[<{tag} class="amount-{sign} {classes}">{content}</{tag}>]],
|
||||
{ tag = tag, sign = amount >= 0 and "pos" or "neg", classes = classes or "", content = s }
|
||||
)
|
||||
end
|
||||
|
||||
local function get_user_theme(username)
|
||||
|
@ -123,10 +146,23 @@ local function get_user_theme(username)
|
|||
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)
|
||||
local unit = nil
|
||||
local n = nil
|
||||
if t > 86400 then
|
||||
n = math.floor(t / 86400)
|
||||
unit = "day"
|
||||
elseif t > 3600 then
|
||||
n = math.floor(t / 3600)
|
||||
unit = "hour"
|
||||
elseif t > 60 then
|
||||
n = math.floor(t / 60)
|
||||
unit = "minute"
|
||||
else
|
||||
n = t
|
||||
unit = "second"
|
||||
end
|
||||
return format("{+time.delta_past}",
|
||||
{ n = n, unit = translations["+time." .. unit .. (n ~= 1 and "s" or "")] })
|
||||
end
|
||||
|
||||
local function respond(status, title, body)
|
||||
|
@ -150,11 +186,11 @@ local function respond(status, title, body)
|
|||
<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>
|
||||
{appname}
|
||||
{+appname}
|
||||
</a>
|
||||
<a href="/?log">{log}</a>
|
||||
<a href="/?products">{products}</a>
|
||||
<a href="/?about">{about}</a>
|
||||
<a href="/?log">{+log}</a>
|
||||
<a href="/?products">{+products}</a>
|
||||
<a href="/?about">{+about}</a>
|
||||
</nav>
|
||||
]], {
|
||||
title = escape(title),
|
||||
|
@ -168,7 +204,7 @@ local function respond(status, title, body)
|
|||
end
|
||||
|
||||
local function error_box(message)
|
||||
return string.format([[<div class="notif error"><p>Error: %s</p></div>]], message)
|
||||
return string.format([[<div class="notif error"><p>Error: %s</p></div>]], escape(message))
|
||||
end
|
||||
|
||||
local function respond_error(message)
|
||||
|
@ -198,9 +234,9 @@ local function read_log()
|
|||
if l == "" or l == nil then
|
||||
return nil
|
||||
end
|
||||
local time, username, amount, pcode, pcount, comment = string.match(l,
|
||||
"(%d+),([%w_ -]+),(-?%d+),([%w_-]*),(-?%d*),([%w_ -]*)")
|
||||
return tonumber(time), username, tonumber(amount), pcode, tonumber(pcount), comment
|
||||
local time, user_src, user_dst, amount, pcode, pcount, comment = string.match(l,
|
||||
format("{time},{user},{user},{amount},{barcode},{amount},{comment}", matchers))
|
||||
return tonumber(time), user_src, user_dst, tonumber(amount), pcode, tonumber(pcount), comment
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -215,22 +251,23 @@ local function read_products()
|
|||
if l == "" or l == nil then
|
||||
return nil
|
||||
end
|
||||
local barcode, price, name, owner = string.match(l, "([%w_-]+),(-?%d+),([%w_ -]*),([%w_ -]*)")
|
||||
return barcode, tonumber(price), name, owner
|
||||
local barcode, price, user, name = string.match(l, "{barcode},{amount},{user},{name}")
|
||||
return barcode, tonumber(price), user, name
|
||||
end
|
||||
end
|
||||
|
||||
local function balances()
|
||||
local users = {}
|
||||
for _, username, amount, _, _, _ in read_log() do
|
||||
users[username] = (users[username] or 0) + amount
|
||||
for _, user_src, user_dst, amount, _, _, _ in read_log() do
|
||||
users[user_src] = (users[user_src] or 0) - amount
|
||||
users[user_dst] = (users[user_dst] or 0) + amount
|
||||
end
|
||||
return users
|
||||
end
|
||||
|
||||
local function product_balances()
|
||||
local products = {}
|
||||
for _, _, _, pcode, pcount, _ in read_log() do
|
||||
for _, _, _, _, pcode, pcount, _ in read_log() do
|
||||
if pcode ~= nil and pcount ~= nil then
|
||||
products[pcode] = (products[pcode] or 0) + pcount
|
||||
end
|
||||
|
@ -240,19 +277,25 @@ end
|
|||
|
||||
local function last_txns()
|
||||
local users = {}
|
||||
for time, username, _, _, _, _ in read_log() do
|
||||
users[username] = time
|
||||
for time, user_src, user_dst, _, _, _, _ in read_log() do
|
||||
users[user_src] = time
|
||||
users[user_dst] = time
|
||||
end
|
||||
return users
|
||||
end
|
||||
|
||||
local function get_active_users()
|
||||
local user_balances = {}
|
||||
for time, username, amount, _, _, _ in read_log() do
|
||||
user_balances[username] = {
|
||||
for time, user_src, user_dst, amount, _, _, _ in read_log() do
|
||||
user_balances[user_src] = {
|
||||
time = time,
|
||||
username = username,
|
||||
balance = (user_balances[username] or { balance = 0 }).balance + amount
|
||||
user_src = user_src,
|
||||
balance = (user_balances[user_src] or { balance = 0 }).balance - amount
|
||||
}
|
||||
user_balances[user_dst] = {
|
||||
time = time,
|
||||
user_dst = user_dst,
|
||||
balance = (user_balances[user_dst] or { balance = 0 }).balance + amount
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -267,62 +310,57 @@ end
|
|||
|
||||
local function r_user_post(username)
|
||||
local data = form_data()
|
||||
local user_src = data.user_src or username
|
||||
local user_dst = data.user_dst
|
||||
local amount = tonumber(data.amount)
|
||||
local pcode = data.pcode
|
||||
local pcount = tonumber(data.pcount)
|
||||
local comment = data.comment
|
||||
local pcode = nil
|
||||
local pcount = nil
|
||||
local powner = nil
|
||||
local powner_comment = nil
|
||||
if data.pcode then
|
||||
for p_barcode, p_amount, p_name, p_owner in read_products() do
|
||||
if p_barcode == data.pcode then
|
||||
powner = p_owner
|
||||
if pcode then
|
||||
local exists = false
|
||||
for p_barcode, p_amount, p_user, p_name in read_products() do
|
||||
if p_barcode == pcode then
|
||||
pcount = (tonumber(data.pcount) or 1) * (data.negate_pcount ~= nil and -1 or 1)
|
||||
pcode = p_barcode
|
||||
if amount == nil then amount = pcount * p_amount end
|
||||
if comment == nil then
|
||||
comment = string.format("%s %d %s", pcount < 0 and "Buy" or "Restock",
|
||||
math.abs(pcount), p_name)
|
||||
|
||||
powner_comment = string.format("%s %d %s %s %s",
|
||||
pcount < 0 and "Sell" or "Restock",
|
||||
math.abs(pcount), p_name,
|
||||
pcount < 0 and "to" or "by",
|
||||
username)
|
||||
end
|
||||
amount = amount or pcount * p_amount
|
||||
user_dst = user_dst or p_user
|
||||
comment = comment or
|
||||
string.format("%s %d %s", pcount < 0 and "Buy" or "Restock", math.abs(pcount or 0), p_name)
|
||||
exists = true
|
||||
end
|
||||
end
|
||||
if amount == nil then
|
||||
if not exists then
|
||||
return error_box("unknown product")
|
||||
end
|
||||
end
|
||||
user_dst = user_dst or "@global"
|
||||
if amount == nil then
|
||||
return error_box("amount invalid")
|
||||
end
|
||||
if comment == nil or comment:match("^[%w_ -]*$") == nil then
|
||||
if comment == nil or comment:match(matchers_global.comment) == nil then
|
||||
return error_box("comment invalid")
|
||||
end
|
||||
if user_src == nil or user_src:match(matchers_global.user) == nil then
|
||||
return error_box("source user invalid")
|
||||
end
|
||||
if user_dst == nil or user_dst:match(matchers_global.user) == nil then
|
||||
return error_box("destination user invalid")
|
||||
end
|
||||
local log = io.open("log", "a+")
|
||||
if log == nil then
|
||||
return error_box("failed to open log")
|
||||
end
|
||||
local time = os.time()
|
||||
-- subtract from buyer
|
||||
log:write(string.format("%d,%s,%d,%s,%s,%s\n", time, username, amount, pcode or "", pcount or "", comment))
|
||||
-- add to owner
|
||||
if powner then
|
||||
-- count is always zero as doesn't affect stock
|
||||
log:write(string.format("%d,%s,%d,%s,%s,%s\n", time, powner, -amount, pcode or "", "", powner_comment))
|
||||
end
|
||||
log:write(string.format("%d,%s,%s,%d,%s,%s,%s\n",
|
||||
time, user_src, user_dst, amount, pcode or "", pcount or "", comment))
|
||||
log:flush()
|
||||
log:close()
|
||||
return format([[
|
||||
<div class="notif"><p>Transaction successful: {amount} ({!comment})</p></div>
|
||||
<div class="notif"><p>{+user.form.transaction.success}: {amount} ({!comment})</p></div>
|
||||
<audio src="{sound}" autoplay></audio>
|
||||
]], {
|
||||
sign = amount >= 0 and "pos" or "neg",
|
||||
amount = format_amount(amount, "strong"),
|
||||
comment = escape(comment),
|
||||
comment = comment,
|
||||
sound = config.transaction_sound or ""
|
||||
})
|
||||
end
|
||||
|
@ -340,12 +378,12 @@ local function r_user(username)
|
|||
balance = balance or 0
|
||||
if notif then print(notif) end
|
||||
if new_user then
|
||||
print([[<div class="notif"><p><i>{user.lazy_creation}</i></p></div>]])
|
||||
print(format([[<div class="notif"><p><i>{+user.lazy_creation}</i></p></div>]]))
|
||||
else
|
||||
print([[<div class="backgroundbox userinfo">]])
|
||||
print(format([[{user.balance}: <br>{amount}<br>]],
|
||||
{ sign = balance >= 0 and "pos" or "neg", amount = format_amount(balance, "span", "balance-value") }))
|
||||
print(format([[{user.last_txn} <a href="/{username}?log">{user.view_log}</a>]],
|
||||
print(format([[{+user.balance}: <br>{amount}<br>]],
|
||||
{ amount = format_amount(balance, "span", "balance-value") }))
|
||||
print(format([[{+user.last_txn} <a href="/{username}?log">{+user.view_log}</a>]],
|
||||
{ time = format_duration(os.time() - last_txn), username = urlencode(username) }))
|
||||
print([[</div>]])
|
||||
end
|
||||
|
@ -353,44 +391,46 @@ local function r_user(username)
|
|||
print([[<div class="amount-presets backgroundbox">]])
|
||||
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 button" />
|
||||
</form>
|
||||
]], amount * type, ({ [-1] = "-", [1] = "+" })[type], amount / 100,
|
||||
({ [-1] = "neg", [1] = "pos" })[type]))
|
||||
local a = amount * type
|
||||
print(format([[<form action="" method="POST">
|
||||
<input type="number" name="amount" id="amount" value="{a_raw}" hidden />
|
||||
<input type="text" name="comment" id="comment" value="" hidden />
|
||||
<input type="submit" value="{amount}" class="amount-{sign} button" />
|
||||
</form>]], {
|
||||
a_raw = a,
|
||||
amount = format_amount(a),
|
||||
sign = a < 0 and "neg" or "pos"
|
||||
}))
|
||||
end
|
||||
end
|
||||
print("</div>")
|
||||
print(format([[
|
||||
<form class="transaction box backgroundbox" action="" method="POST">
|
||||
<h3>{user.form.transaction}</h3>
|
||||
<h3>{+user.form.transaction}</h3>
|
||||
<label for="amount">Amount (ct): </label>
|
||||
<input type="number" name="amount" id="amount" />
|
||||
<label for="comment">Comment: </label>
|
||||
<input type="text" name="comment" id="comment" />
|
||||
<input type="submit" value="{user.form.transaction.submit}" class="amount-ntr button" />
|
||||
<input type="submit" value="{+user.form.transaction.submit}" class="amount-ntr button" />
|
||||
</form>
|
||||
<form class="transaction box backgroundbox" action="" method="POST" id="buy_product">
|
||||
<h3>{user.form.buy}</h3>
|
||||
<h3>{+user.form.buy}</h3>
|
||||
<input type="text" name="negate_pcount" value="1" hidden />
|
||||
<label for="pcount">Count: </label>
|
||||
<input type="number" name="pcount" id="pcount" value="1" />
|
||||
<label for="pcode">{field.barcode}: </label>
|
||||
<label for="pcode">{+field.barcode}: </label>
|
||||
<input type="text" name="pcode" id="pcode" />
|
||||
<input class="amount-neg button" type="submit" value="{user.form.buy.submit}" />
|
||||
<input class="amount-neg button" type="submit" value="{+user.form.buy.submit}" />
|
||||
</form>
|
||||
<form class="transaction box backgroundbox" action="" method="POST" id="buy_product">
|
||||
<h3>{user.form.restock}</h3>
|
||||
<label for="pcount">{field.count}: </label>
|
||||
<h3>{+user.form.restock}</h3>
|
||||
<label for="pcount">{+field.count}: </label>
|
||||
<input type="number" name="pcount" id="pcount" value="1" />
|
||||
<label for="amount">{field.upstream_price}: </label>
|
||||
<label for="amount">{+field.upstream_price}: </label>
|
||||
<input type="number" name="amount" id="amount" />
|
||||
<label for="pcode">{field.barcode}: </label>
|
||||
<label for="pcode">{+field.barcode}: </label>
|
||||
<input type="text" name="pcode" id="pcode" />
|
||||
<input type="submit" value="{user.form.restock.submit}" class="button amount-pos" />
|
||||
<input type="submit" value="{+user.form.restock.submit}" class="button amount-pos" />
|
||||
</form>
|
||||
]]))
|
||||
print("</div>")
|
||||
|
@ -399,46 +439,52 @@ end
|
|||
|
||||
local function r_log(filter)
|
||||
return respond(200, "Abrechnungen", function()
|
||||
print([[<table class="log" printtitle="Abrechnung"]])
|
||||
print([[<thead><tr>
|
||||
<th>Time</th>
|
||||
<th>Username</th>
|
||||
<th>Amount</th>
|
||||
<th>P.-Barcode</th>
|
||||
<th>P.-Count</th>
|
||||
<th>Comment</th>
|
||||
<th>Actions</th>
|
||||
</tr></thead>]])
|
||||
print([[<table class="log"]])
|
||||
print(format([[<thead><tr>
|
||||
<th>{+field.time}</th>
|
||||
<th>{+field.username}</th>
|
||||
<th>{+field.amount}</th>
|
||||
<th>{+field.barcode}</th>
|
||||
<th>{+field.count}</th>
|
||||
<th>{+field.comment}</th>
|
||||
<th>{+log.actions}</th>
|
||||
</tr></thead>]]))
|
||||
print("<tbody>")
|
||||
for time, username, amount, pcode, pcount, comment in read_log() do
|
||||
if filter == nil or filter == username then
|
||||
print(string.format([[
|
||||
for time, user_src, user_dst, amount, pcode, pcount, comment in read_log() do
|
||||
if filter == nil or filter == user_src or filter == user_dst then
|
||||
print(format([[
|
||||
<tr>
|
||||
<td>%s (%s ago)</td>
|
||||
<td>%s</td>
|
||||
<td class="amount-%s">%.02f€</td>
|
||||
<td>%s</td>
|
||||
<td>%s %s</td>
|
||||
<td>%s</td>
|
||||
<td>{time} ({time_delta})</td>
|
||||
<td>{user_src} → {user_dst</td>
|
||||
{amount}
|
||||
<td>{pcode}</td>
|
||||
<td>{pcount}</td>
|
||||
<td>{comment}</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 />
|
||||
<form action="/?transaction" method="POST">
|
||||
<input type="number" name="user_src" value="{user_src}" hidden />
|
||||
<input type="number" name="user_dst" value="{user_dst}" hidden />
|
||||
<input type="number" name="amount" value="{revert_amount}" hidden />
|
||||
<input type="number" name="pcode" value="{pcode}" hidden />
|
||||
<input type="number" name="pcount" value="{revert_pcount}" hidden />
|
||||
<input type="text" name="comment" value="Revert {comment}" hidden />
|
||||
<input type="submit" class="amount-ntr button" value="Revert" />
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
]],
|
||||
os.date("!%Y-%m-%dT%H:%M:%SZ", time), format_duration(os.time() - time),
|
||||
escape(username),
|
||||
amount >= 0 and "pos" or "neg", amount / 100,
|
||||
escape(pcode) or "",
|
||||
pcount and (pcount < 0 and "buy" or "stock") or "", pcount and tostring(math.abs(pcount)) or "",
|
||||
escape(comment),
|
||||
escape(username),
|
||||
-amount,
|
||||
escape(comment)
|
||||
))
|
||||
]], {
|
||||
time = os.date("!%Y-%m-%dT%H:%M:%SZ", time),
|
||||
time_delta = format_duration(os.time() - time),
|
||||
user_src = escape(user_src),
|
||||
user_dst = escape(user_dst),
|
||||
amount = format_amount(amount, "td"),
|
||||
pcode = escape(pcode),
|
||||
pcount = (pcount and (pcount < 0 and "buy " or "stock ") or "") ..
|
||||
(pcount and tostring(math.abs(pcount)) or ""),
|
||||
comment = escape(comment),
|
||||
revert_amount = -amount,
|
||||
revert_pcount = -pcount,
|
||||
}))
|
||||
end
|
||||
end
|
||||
print("</tbody>")
|
||||
|
@ -448,40 +494,35 @@ end
|
|||
|
||||
local function r_index()
|
||||
return respond(200, "Abrechenbarkeit", function()
|
||||
print([[
|
||||
print(format([[
|
||||
<form action="/" method="GET" id="user_creation">
|
||||
<h3>User Creation</h3>
|
||||
<label for="username">Username: </label>
|
||||
<h3>{+index.form.create_user}</h3>
|
||||
<label for="username">{+field.username}: </label>
|
||||
<input type="text" name="create_user" id="username" />
|
||||
<input type="submit" value="Continue" class="button amount-ntr" />
|
||||
<input type="submit" value="{+index.form.create_user.submit}" class="button amount-ntr" />
|
||||
</form>
|
||||
]])
|
||||
print([[<div class="userlist" printtitle="User List, is still work in progress :/"></div>]]) -- for printing
|
||||
]]))
|
||||
print([[<div class="userlist"></div>]]) -- for printing
|
||||
print([[<ul class="userlist">]])
|
||||
for _, user in ipairs(get_active_users()) do
|
||||
print(string.format([[
|
||||
<li><a href="/%s"><span class="name">%s</span> <span class="amount amount-%s">%.02f€</span></a></li>
|
||||
]],
|
||||
urlencode(user.username),
|
||||
escape(user.username),
|
||||
user.balance >= 0 and "pos" or "neg", user.balance / 100
|
||||
))
|
||||
print(format([[<li>
|
||||
<a href="/{username_url}">
|
||||
<span class="name">{!username}</span>
|
||||
{balance}
|
||||
</a>
|
||||
</li>]], {
|
||||
username_url = urlencode(user.username),
|
||||
username = user.username,
|
||||
balance = format_amount(user.balance, "span")
|
||||
}))
|
||||
end
|
||||
print("</ul>")
|
||||
end)
|
||||
end
|
||||
|
||||
local function validate_username(username)
|
||||
-- disallow leading or traling whitespace
|
||||
return username ~= nil
|
||||
and username:match("^([%w_ -]+)$") ~= nil
|
||||
and username:match("^%s") == nil
|
||||
and username:match("%s$") == nil
|
||||
end
|
||||
|
||||
local function r_create_user()
|
||||
local username = query.create_user
|
||||
if not validate_username(username) then
|
||||
if username:match(matchers_global.user) == nil then
|
||||
return respond_error("invalid username " .. username)
|
||||
end
|
||||
return redirect(string.format("/%s", urlencode(username)))
|
||||
|
@ -490,10 +531,7 @@ end
|
|||
local function r_products_post()
|
||||
local data = form_data()
|
||||
local barcode = data.barcode
|
||||
if barcode == nil then
|
||||
return error_box("barcode unset")
|
||||
end
|
||||
if barcode:match("^[%w_-]*$") == nil then
|
||||
if barcode == nil or barcode:match("^[%w_-]*$") == nil then
|
||||
return error_box("barcode invalid")
|
||||
end
|
||||
if data.delete then
|
||||
|
@ -501,9 +539,9 @@ local function r_products_post()
|
|||
if new_products == nil then
|
||||
return error_box("failed to open new products")
|
||||
end
|
||||
for a_barcode, price, name, owner in read_products() do
|
||||
for a_barcode, price, user, name in read_products() do
|
||||
if barcode ~= a_barcode then
|
||||
new_products:write(string.format("%s,%d,%s,%s\n", a_barcode, price, name, owner))
|
||||
new_products:write(string.format("%s,%d,%s,%s\n", a_barcode, price, user, name))
|
||||
end
|
||||
end
|
||||
new_products:flush()
|
||||
|
@ -512,21 +550,21 @@ local function r_products_post()
|
|||
else
|
||||
local price = tonumber(data.price)
|
||||
local name = data.name
|
||||
local user = data.user
|
||||
if price == nil then
|
||||
return error_box("price invalid")
|
||||
end
|
||||
if name:match("^[%w_ -]*$") == nil then
|
||||
if name == nil or name:match(matchers_global.name) == nil then
|
||||
return error_box("name invalid")
|
||||
end
|
||||
if user == nil or user:match(matchers_global.user) == nil then
|
||||
return error_box("user invalid")
|
||||
end
|
||||
local products = io.open("products", "a+")
|
||||
if products == nil then
|
||||
return error_box("failed to open products")
|
||||
end
|
||||
local owner = data.owner or ""
|
||||
if name:match("^[%w_ -]*$") == nil then
|
||||
return error_box("owner invalid")
|
||||
end
|
||||
products:write(string.format("%s,%d,%s,%s\n", barcode, price, name, owner))
|
||||
products:write(string.format("%s,%d,%s,%s\n", barcode, price, user, name))
|
||||
products:flush()
|
||||
products:close()
|
||||
end
|
||||
|
@ -548,8 +586,8 @@ local function r_products()
|
|||
<input type="text" name="name" id="name" />
|
||||
<label for="price">Price (ct): </label>
|
||||
<input type="number" name="price" id="price" />
|
||||
<label for="owner">Owner: </label>
|
||||
<input type="text" name="owner" id="owner" />
|
||||
<label for="user">User: </label>
|
||||
<input type="text" name="user" id="user" />
|
||||
<label for="barcode">Barcode: </label>
|
||||
<input type="text" name="barcode" id="barcode" />
|
||||
<input type="submit" value="Add" class="amount-ntr button" />
|
||||
|
@ -568,19 +606,23 @@ local function r_products()
|
|||
<th>Price</th>
|
||||
<th>Barcode</th>
|
||||
<th>Count</th>
|
||||
<th>Owner</th>
|
||||
<th>User</th>
|
||||
</tr>]])
|
||||
local pbals = product_balances()
|
||||
for barcode, price, name, owner in read_products() do
|
||||
print(string.format([[
|
||||
<tr><td>%s</td><td class="amount-%s">%.02f€</td><td>%s</td><td>%s</td><td>%s</td></tr>
|
||||
]],
|
||||
name,
|
||||
-price >= 0 and "pos" or "neg", -price / 100,
|
||||
barcode,
|
||||
pbals[barcode] or "0",
|
||||
owner
|
||||
))
|
||||
for barcode, price, user, name in read_products() do
|
||||
print(string.format([[<tr>
|
||||
<td>{!name}</td>
|
||||
{price}
|
||||
<td>{!barcode}</td>
|
||||
<td>{!count}</td>
|
||||
<td>{!user}</td>
|
||||
</tr>]], {
|
||||
name = name,
|
||||
price = format_amount(-price),
|
||||
barcode = barcode,
|
||||
count = pbals[barcode] or "0",
|
||||
user = user,
|
||||
}))
|
||||
end
|
||||
print("</table>")
|
||||
end)
|
||||
|
@ -588,13 +630,17 @@ end
|
|||
|
||||
local function r_about()
|
||||
respond(200, "About Abrechenbarkeit", function()
|
||||
print([[
|
||||
<h1>About Abrechenbarkeit</h1>
|
||||
<p>Abrechenbarkeit is a simple trust-based ledger for keeping track of money spent on product.</p>
|
||||
<p>Abrechenbarkeit is free software. It is licensed exclusively GNU Affero General Public License Version 3 only.<p>
|
||||
<p>The source code is published on <a href="https://codeberg.org/metamuffin/strichliste">Codeberg</a>. This is also where <a href="https://codeberg.org/metamuffin/strichliste/issues">issues with this software</a> should be reported.</p>
|
||||
<p>Thanks for choosing Abrechenbarkeit.</p>
|
||||
]])
|
||||
print(format([[
|
||||
<h1>{+about.title}</h1>
|
||||
<p>{+about.desc}</p>
|
||||
<p>{+about.license}<p>
|
||||
<p>{+about.source}</p>
|
||||
<p>{+about.thanks}</p>
|
||||
]], {
|
||||
issues = [[<a href="https://codeberg.org/metamuffin/strichliste/issues">]],
|
||||
codeberg = [[<a href="https://codeberg.org/metamuffin/strichliste">]],
|
||||
ae = [[</a>]],
|
||||
}))
|
||||
end)
|
||||
end
|
||||
|
||||
|
@ -603,7 +649,7 @@ local function extract_username()
|
|||
return respond_error("no path")
|
||||
end
|
||||
local username = urldecode(path:sub(2))
|
||||
if username == nil or username:match("^([%w_ -]+)$") == nil then
|
||||
if username == nil or username:match(matchers_global.user) == nil then
|
||||
return nil
|
||||
end
|
||||
return username
|
||||
|
@ -623,7 +669,7 @@ if path == "/" then
|
|||
end
|
||||
else
|
||||
local username = extract_username()
|
||||
if username == nil or not validate_username(username) then
|
||||
if username == nil then
|
||||
return respond_error("username invalid")
|
||||
elseif query.log then
|
||||
return r_log(username)
|
||||
|
|
|
@ -20,19 +20,19 @@ products.form.title=Produkt Liste
|
|||
products=Produkte
|
||||
time.day=Tag
|
||||
time.days=Tage
|
||||
time.delta_past=vor %d %s
|
||||
time.delta_past=vor {n} {unit}
|
||||
time.hour=Stunde
|
||||
time.hours=Stunden
|
||||
time.minute=Minute
|
||||
time.minutes=Minuten
|
||||
time.second=Sekunde
|
||||
time.seconds=Sekunden
|
||||
user.balance=Kontostand:
|
||||
user.balance=Kontostand
|
||||
user.form.buy.submit=Kaufen
|
||||
user.form.buy=Produkt Kaufen
|
||||
user.form.restock.submit=Wiederauffüllen
|
||||
user.form.restock=Produkt wiederauffüllen
|
||||
user.form.transaction.submit=Aktualisieren
|
||||
user.form.transaction=Transaktion erstellen
|
||||
user.last_txn=Letzte Transaktion %s.
|
||||
user.view_log=User Abrechnung
|
||||
user.last_txn=Letzte Transaktion {time}.
|
||||
user.view_log=Benutzerprotokoll einsehen
|
||||
|
|
|
@ -7,9 +7,15 @@ field.comment=Comment
|
|||
field.count=Count
|
||||
field.name=Name
|
||||
field.price=Price
|
||||
field.time=Time
|
||||
field.upstream_price=Upstream Price
|
||||
field.username=Username
|
||||
index.form.create_user.submit=Continue
|
||||
about.desc=Abrechenbarkeit is a simple trust-based ledger for keeping track of money spent on product.
|
||||
about.title=About Abrechenbarkeit
|
||||
about.license=Abrechenbarkeit is free software. It is licensed exclusively GNU Affero General Public License Version 3 only.
|
||||
about.source=The source code is published on {codeberg}Codeberg{ae}. This is also where {issues}issues with this software{ae} should be reported.
|
||||
about.thanks=Thanks for choosing Abrechenbarkeit.
|
||||
index.form.create_user=User Creation
|
||||
log.actions.revert=Revert
|
||||
log.actions=Actions
|
||||
|
@ -20,7 +26,7 @@ products.form.title=Product List
|
|||
products=Products
|
||||
time.day=day
|
||||
time.days=days
|
||||
time.delta_past=%d %s ago
|
||||
time.delta_past={n} {unit} ago
|
||||
time.hour=hour
|
||||
time.hours=hours
|
||||
time.minute=minute
|
||||
|
@ -37,3 +43,4 @@ user.form.transaction=Create Transaction
|
|||
user.last_txn=Last transaction added {time}.
|
||||
user.view_log=View user log
|
||||
user.lazy_creation=This user account does not exist yet. It will only be created after the first transaction.
|
||||
user.form.transaction.success=Transaction successful
|
||||
|
|
|
@ -12,7 +12,7 @@ useful for development or proxyless deployments.
|
|||
|
||||
## Data Files
|
||||
|
||||
- `log` stores the transaction log as CSV (`time,user,amount,comment`)
|
||||
- `products` stores the product list as CSV (`barcode,price,name`)
|
||||
- `log` stores the transaction log as CSV (`time,user_a,user_b,amount,pcode,pcount,comment`)
|
||||
- `products` stores the product list as CSV (`barcode,price,user,name`)
|
||||
- `config` stores configuration parameters as ESV (`key=value`)
|
||||
- `transaction_sound`: URL to sound played when creating a transaction
|
||||
|
|
Loading…
Reference in a new issue