all repos — piggy @ 693d77e1f7f4d3cc8f8da2e000d408ec3b1950ac

Dead simple finance manager in Go, HTML and JS.

add edit pages for bookmakers and accounts
Marco Andronaco andronacomarco@gmail.com
Sun, 06 Oct 2024 12:46:05 +0200
commit

693d77e1f7f4d3cc8f8da2e000d408ec3b1950ac

parent

8a31a262f27c077d8a41170129e08c147f5404b9

M src/api/api.gosrc/api/api.go

@@ -20,6 +20,24 @@

jsonResponse(w, bookmakers) } +func getBookmakersId(w http.ResponseWriter, r *http.Request) { + id, err := getId(r) + if err != nil { + new400Error(w, err) + return + } + + var bookmaker app.Bookmaker + err = app.DB.First(&bookmaker, id).Error + if err != nil { + log.Println("could not get bookmaker: " + err.Error()) + new500Error(w, err) + return + } + + jsonResponse(w, bookmaker) +} + func postBookmakers(w http.ResponseWriter, r *http.Request) { var bookmaker app.Bookmaker err := json.NewDecoder(r.Body).Decode(&bookmaker)

@@ -48,6 +66,24 @@ return

} jsonResponse(w, accounts) +} + +func getAccountsId(w http.ResponseWriter, r *http.Request) { + id, err := getId(r) + if err != nil { + new400Error(w, err) + return + } + + var account app.Account + err = app.DB.First(&account, id).Error + if err != nil { + log.Println("could not get account: " + err.Error()) + new500Error(w, err) + return + } + + jsonResponse(w, account) } func postAccounts(w http.ResponseWriter, r *http.Request) {
M src/api/routes.gosrc/api/routes.go

@@ -15,9 +15,11 @@

http.Handle("GET /", http.FileServer(http.Dir("static"))) http.HandleFunc("GET /api/bookmakers", getBookmakers) + http.HandleFunc("GET /api/bookmakers/{id}", getBookmakersId) http.HandleFunc("POST /api/bookmakers", postBookmakers) http.HandleFunc("GET /api/accounts", getAccounts) + http.HandleFunc("GET /api/accounts/{id}", getAccountsId) http.HandleFunc("POST /api/accounts", postAccounts) http.HandleFunc("GET /api/records", getRecords)
M src/app/models.gosrc/app/models.go

@@ -17,6 +17,7 @@

type Account struct { ID uint `json:"id"` CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` Name string `json:"name" gorm:"not null"` }
A static/accounts/edit/index.html

@@ -0,0 +1,27 @@

+<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Piggy - Accounts</title> + <link rel="stylesheet" href="/css/styles.css"> +</head> + +<body> + <header> + <nav></nav> + </header> + <main> + <h1>Accounts > Edit</h1> + + <div id="main-container"> + </div> + + <button id="save">Save</button> + </main> + <script src="/js/common.js"></script> + <script src="/js/accounts-edit.js"></script> +</body> + +</html>
M static/accounts/index.htmlstatic/accounts/index.html

@@ -16,10 +16,17 @@ <main>

<h1>Accounts</h1> <div id="main-container"> - Accounts will be shown here. + <table> + <thead id="accounts-header"></thead> + <tbody id="accounts-table"></tbody> + <tfoot></tfoot> + </table> </div> + + <button id="new-account">New Account</button> </main> <script src="/js/common.js"></script> + <script src="/js/accounts.js"></script> </body> </html>
A static/bookmakers/edit/index.html

@@ -0,0 +1,27 @@

+<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Piggy - Bookmakers</title> + <link rel="stylesheet" href="/css/styles.css"> +</head> + +<body> + <header> + <nav></nav> + </header> + <main> + <h1>Bookmakers > Edit</h1> + + <div id="main-container"> + </div> + + <button id="save">Save</button> + </main> + <script src="/js/common.js"></script> + <script src="/js/bookmakers-edit.js"></script> +</body> + +</html>
M static/bookmakers/index.htmlstatic/bookmakers/index.html

@@ -16,10 +16,17 @@ <main>

<h1>Bookmakers</h1> <div id="main-container"> - Bookmakers will be shown here. + <table> + <thead id="bookmakers-header"></thead> + <tbody id="bookmakers-table"></tbody> + <tfoot></tfoot> + </table> </div> + + <button id="new-bookmaker">New Bookmaker</button> </main> <script src="/js/common.js"></script> + <script src="/js/bookmakers.js"></script> </body> </html>
A static/js/accounts-edit.js

@@ -0,0 +1,45 @@

+document.addEventListener('DOMContentLoaded', function () { + handleID(); + + document.getElementById('save').addEventListener('click', submit); +}); + +let id; + +async function handleID() { + id = getQueryStringID(); + const record = id === 0 ? null : await getAccount(id); + document.getElementById("main-container").appendChild(loadAccount(record)); +} + +function loadAccount(account) { + const div = document.createElement("div"); + div.setAttribute("data-type", "account"); + div.setAttribute("data-id", id); + div.classList.add("account"); + + // account.name + div.appendChild(newInputText("Name", account?.name, "account-name")); + + return div; +} + +function getInputValueFromNode(node, name) { + const element = node.getElementsByClassName(name)[0]; + return element.type === "checkbox" ? element.checked : element.value; +} + +function buildAccountObject() { + const node = document.getElementsByClassName("account")[0]; + return { + id: +node.getAttribute("data-id"), + name: getInputValueFromNode(node, "account-name"), + } +} + +async function submit() { + if (await saveAccount(buildAccountObject())) { + location.href = "/accounts" + } +} +
A static/js/accounts.js

@@ -0,0 +1,53 @@

+document.addEventListener('DOMContentLoaded', function () { + loadAccounts(); + + document.getElementById('new-account').addEventListener('click', editAccount); +}); + +function editAccount() { + let out = "/accounts/edit/"; + const id = this.getAttribute("data-id"); + if (id) { + out += "?id=" + id; + } + location.href = out; +} + +function loadAccounts() { + getAccounts().then(accounts => { + const header = document.getElementById('accounts-header'); + const table = document.getElementById('accounts-table'); + header.innerHTML = ''; + table.innerHTML = ''; + + const tr = document.createElement('tr'); + const headers = ["ID", "Created", "Updated", "Name"]; + + for (const header of headers) { + const td = document.createElement('td'); + td.innerText = header; + tr.appendChild(td); + } + header.appendChild(tr); + + for (const account of accounts) { + const tr = document.createElement('tr'); + tr.setAttribute("data-id", account.id); + tr.onclick = editAccount; + + const fields = [ + account.id, + formatDate(account.created_at), + formatDate(account.updated_at), + account.name, + ]; + + for (const field of fields) { + const td = document.createElement('td'); + td.innerHTML = field; + tr.appendChild(td); + } + table.appendChild(tr); + } + }); +}
A static/js/bookmakers-edit.js

@@ -0,0 +1,53 @@

+document.addEventListener('DOMContentLoaded', function () { + handleID(); + + document.getElementById('save').addEventListener('click', submit); +}); + +let id; + +async function handleID() { + id = getQueryStringID(); + const record = id === 0 ? null : await getBookmaker(id); + document.getElementById("main-container").appendChild(loadBookmaker(record)); +} + +function loadBookmaker(bookmaker) { + const div = document.createElement("div"); + div.setAttribute("data-type", "bookmaker"); + div.setAttribute("data-id", id); + div.classList.add("bookmaker"); + + // bookmaker.name + div.appendChild(newInputText("Name", bookmaker?.name, "bookmaker-name")); + + // bookmaker.exchange + div.appendChild(newInputCheckbox("Exchange", bookmaker?.exchange, "bookmaker-exchange")); + + // bookmaker.default_commission + div.appendChild(newInputText("Commission", bookmaker?.default_commission, "bookmaker-default_commission")); + + return div; +} + +function getInputValueFromNode(node, name) { + const element = node.getElementsByClassName(name)[0]; + return element.type === "checkbox" ? element.checked : element.value; +} + +function buildBookmakerObject() { + const node = document.getElementsByClassName("bookmaker")[0]; + return { + id: +node.getAttribute("data-id"), + name: getInputValueFromNode(node, "bookmaker-name"), + exchange: getInputValueFromNode(node, "bookmaker-exchange"), + default_commission: +getInputValueFromNode(node, "bookmaker-default_commission"), + } +} + +async function submit() { + if (await saveBookmaker(buildBookmakerObject())) { + location.href = "/bookmakers" + } +} +
A static/js/bookmakers.js

@@ -0,0 +1,55 @@

+document.addEventListener('DOMContentLoaded', function () { + loadBookmakers(); + + document.getElementById('new-bookmaker').addEventListener('click', editBookmaker); +}); + +function editBookmaker() { + let out = "/bookmakers/edit/"; + const id = this.getAttribute("data-id"); + if (id) { + out += "?id=" + id; + } + location.href = out; +} + +function loadBookmakers() { + getBookmakers().then(bookmakers => { + const header = document.getElementById('bookmakers-header'); + const table = document.getElementById('bookmakers-table'); + header.innerHTML = ''; + table.innerHTML = ''; + + const tr = document.createElement('tr'); + const headers = ["ID", "Created", "Updated", "Name", "Exchange", "Commission"]; + + for (const header of headers) { + const td = document.createElement('td'); + td.innerText = header; + tr.appendChild(td); + } + header.appendChild(tr); + + for (const bookmaker of bookmakers) { + const tr = document.createElement('tr'); + tr.setAttribute("data-id", bookmaker.id); + tr.onclick = editBookmaker; + + const fields = [ + bookmaker.id, + formatDate(bookmaker.created_at), + formatDate(bookmaker.updated_at), + bookmaker.name, + formatBoolean(bookmaker.exchange), + bookmaker.default_commission, + ]; + + for (const field of fields) { + const td = document.createElement('td'); + td.innerHTML = field; + tr.appendChild(td); + } + table.appendChild(tr); + } + }); +}
M static/js/common.jsstatic/js/common.js

@@ -8,6 +8,7 @@ navObject.appendChild(a)

} }); +// Global constants const navPages = [ { name: "Home", href: "/" }, { name: "Bookmakers", href: "/bookmakers" },

@@ -18,6 +19,7 @@

const currency = "€"; const locale = "it-IT"; +// Cell formatters function formatValue(v) { return (v / 100).toFixed(2); }

@@ -30,16 +32,17 @@ function formatDate(dateString) {

return (new Date(dateString)).toLocaleString(locale); } -function formatDone(value, id) { +function formatBoolean(value, id) { const input = document.createElement("input"); input.type = "checkbox"; - input.checked = value; + if (value) input.setAttribute("checked", ""); input.disabled = true; //input.setAttribute("data-id", id); //input.onchange = undefined; return input.outerHTML; } +// Input components function newInputText(label, value, name) { const l = document.createElement("label"); const input = document.createElement("input");

@@ -57,12 +60,17 @@ const l = document.createElement("label");

const input = document.createElement("input"); input.className = name; input.type = "checkbox"; - input.checked = value ?? false; + if (value) input.setAttribute("checked", ""); l.appendChild(input); l.innerHTML += label; return l; } +// Functions +function getQueryStringID() { + return Number(new URLSearchParams(window.location.search).get("id") ?? 0); +} + async function handleFetchResult(res) { if (!res.ok) { console.error(await res.text())

@@ -86,6 +94,39 @@ });

return await handleFetchResult(res); } +// API calls +async function getBookmakers() { + return await myFetch('/api/bookmakers'); +} + +async function getAccounts() { + return await myFetch('/api/accounts'); +} + async function getRecords() { return await myFetch('/api/records'); } + +async function getBookmaker(id) { + return await myFetch(`/api/bookmakers/${id}`); +} + +async function getAccount(id) { + return await myFetch(`/api/accounts/${id}`); +} + +async function getRecord(id) { + return await myFetch(`/api/records/${id}`); +} + +async function saveBookmaker(payload) { + return await myFetchPOST("/api/bookmakers", payload); +} + +async function saveAccount(payload) { + return await myFetchPOST("/api/accounts", payload); +} + +async function saveRecord(payload) { + return await myFetchPOST("/api/records", payload); +}
M static/js/records-edit.jsstatic/js/records-edit.js

@@ -1,18 +1,15 @@

document.addEventListener('DOMContentLoaded', function () { handleID(); - document.getElementById('save').addEventListener('click', saveRecord); + document.getElementById('save').addEventListener('click', submit); }); let id; async function handleID() { - id = Number(new URLSearchParams(window.location.search).get("id") ?? 0); - const record = id === 0 ? null : await myFetch(`/api/records/${id}`); - - document.getElementById("main-container").appendChild(loadRecord(record)) - - console.log(record); + id = getQueryStringID(); + const record = id === 0 ? null : await getRecord(id); + document.getElementById("main-container").appendChild(loadRecord(record)); } function loadRecord(record) {

@@ -153,9 +150,8 @@ }

return result; } -async function saveRecord() { - const result = await myFetchPOST("/api/records", buildRecordObject()); - if (result) { - alert("Done"); +async function submit() { + if (await saveRecord(buildRecordObject())) { + location.href = "/records" } }
M static/js/records.jsstatic/js/records.js

@@ -5,7 +5,7 @@ document.getElementById('new-record').addEventListener('click', editRecord);

}); function editRecord() { - const out = "/records/edit/"; + let out = "/records/edit/"; const id = this.getAttribute("data-id"); if (id) { out += "?id=" + id;

@@ -37,7 +37,7 @@ tr.onclick = editRecord;

const fields = [ formatDate(record.created_at), - formatDone(record.done, record.id), + formatBoolean(record.done, record.id), record.type, record.description, formatDate(record.date),