all repos — piggy @ 8a31a262f27c077d8a41170129e08c147f5404b9

Dead simple finance manager in Go, HTML and JS.

add records/edit page
Marco Andronaco andronacomarco@gmail.com
Sun, 06 Oct 2024 02:16:45 +0200
commit

8a31a262f27c077d8a41170129e08c147f5404b9

parent

83d9cee1226a9df3b762f3507bbb619f6004b5e9

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

@@ -78,6 +78,22 @@

jsonResponse(w, records) } +func getRecordsId(w http.ResponseWriter, r *http.Request) { + id, err := getId(r) + if err != nil { + new400Error(w, err) + return + } + + record, err := app.GetRecord(id) + if err != nil { + log.Println("could not get record: " + err.Error()) + new500Error(w, err) + } + + jsonResponse(w, record) +} + func postRecords(w http.ResponseWriter, r *http.Request) { var record app.Record err := json.NewDecoder(r.Body).Decode(&record)
M src/api/routes.gosrc/api/routes.go

@@ -21,6 +21,7 @@ http.HandleFunc("GET /api/accounts", getAccounts)

http.HandleFunc("POST /api/accounts", postAccounts) http.HandleFunc("GET /api/records", getRecords) + http.HandleFunc("GET /api/records/{id}", getRecordsId) http.HandleFunc("POST /api/records", postRecords) log.Println("Serving at " + address + "...")
M src/api/utils.gosrc/api/utils.go

@@ -4,6 +4,7 @@ import (

"encoding/json" "log" "net/http" + "strconv" ) type APIError struct {

@@ -25,3 +26,12 @@

func new400Error(w http.ResponseWriter, err error) { http.Error(w, err.Error(), http.StatusBadRequest) } + +func getId(r *http.Request) (id uint, err error) { + v := r.PathValue("id") + id64, err := strconv.ParseUint(v, 10, 0) + if err != nil { + return + } + return uint(id64), nil +}
M src/app/records.gosrc/app/records.go

@@ -109,3 +109,13 @@

records, total = FillRecordValues(records) return } + +func GetRecord(id uint) (record Record, err error) { + err = DB.Preload("Entries.SubEntries").First(&record, id).Error + if err != nil { + return + } + + records, _ := FillRecordValues([]Record{record}) + return records[0], nil +}
M static/css/styles.cssstatic/css/styles.css

@@ -1,7 +1,9 @@

body { font-family: Arial, sans-serif; - margin: 20px; + margin-top: 20px; background-color: beige; + max-width: 800px; + margin-inline: auto; } nav > a:link, nav > a:visited {

@@ -17,12 +19,29 @@ #main-container {

margin-top: 20px; } +#main-container > input, #edit-container > label { + margin-right: 20px; +} + +.record, .entry, .subentry { + margin-top: 20px; + background-color: rgba(0, 0, 0, 0.05); +} + +label { + display: inline-block; +} + thead { font-weight: bold; } table { width: 100%; +} + +table td, table th { + padding: 10px; /* Adjust the padding value as needed */ } button {
M static/js/common.jsstatic/js/common.js

@@ -40,6 +40,29 @@ //input.onchange = undefined;

return input.outerHTML; } +function newInputText(label, value, name) { + const l = document.createElement("label"); + const input = document.createElement("input"); + input.className = name; + input.type = "text"; + input.placeholder = label; + input.value = value ?? ""; + l.innerHTML += label + "<br />"; + l.appendChild(input); + return l; +} + +function newInputCheckbox(label, value, name) { + const l = document.createElement("label"); + const input = document.createElement("input"); + input.className = name; + input.type = "checkbox"; + input.checked = value ?? false; + l.appendChild(input); + l.innerHTML += label; + return l; +} + async function handleFetchResult(res) { if (!res.ok) { console.error(await res.text())
A static/js/records-edit.js

@@ -0,0 +1,161 @@

+document.addEventListener('DOMContentLoaded', function () { + handleID(); + + document.getElementById('save').addEventListener('click', saveRecord); +}); + +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); +} + +function loadRecord(record) { + const div = document.createElement("div"); + div.setAttribute("data-type", "record"); + div.setAttribute("data-id", id); + div.classList.add("record"); + + // record.type + div.appendChild(newInputText("Type", record?.type, "record-type")); + + // record.description + div.appendChild(newInputText("Description", record?.description, "record-description")); + + // record.done + div.appendChild(newInputCheckbox("Done", record?.done, "record-done")); + + // record.entries + div.appendChild(loadEntries(record?.entries ?? [null], "record-entries")); + + return div; +} + +function loadEntries(entries, name) { + const div = document.createElement("div") + div.className = name; + for (const entry of entries) { + div.appendChild(loadEntry(entry)); + } + return div; +} + +function loadEntry(entry) { + const div = document.createElement("div"); + div.setAttribute("data-type", "entry"); + div.setAttribute("data-id", entry?.id ?? 0); + div.classList.add("entry"); + + // entry.bookmaker_id + div.appendChild(newInputText("Bookmaker ID", entry?.bookmaker_id, "entry-bookmaker_id")) + + // entry.account_id + div.appendChild(newInputText("Account ID", entry?.account_id, "entry-account_id")) + + // entry.amount + div.appendChild(newInputText("Amount", entry?.amount, "entry-amount")) + + // entry.refund + div.appendChild(newInputText("Refund", entry?.refund, "entry-refund")) + + // entry.bonus + div.appendChild(newInputText("Bonus", entry?.bonus, "entry-bonus")) + + // entry.commission + div.appendChild(newInputText("Commission", entry?.commission, "entry-commission")) + + // entry.sub_entries + div.appendChild(loadSubEntries(entry?.sub_entries ?? [null], "entry-subentries")); + + return div; +} + +function loadSubEntries(subEntries, name) { + const div = document.createElement("div") + div.className = name; + for (const subEntry of subEntries) { + div.appendChild(loadSubEntry(subEntry)); + } + return div; +} + +function loadSubEntry(subEntry) { + const div = document.createElement("div"); + div.setAttribute("data-type", "subentry"); + div.setAttribute("data-id", subEntry?.id ?? 0); + div.classList.add("subentry"); + + // subentry.description + div.appendChild(newInputText("Description", subEntry?.description, "subentry-description")); + + // subentry.odds + div.appendChild(newInputText("Odds", subEntry?.odds, "subentry-odds")) + + // subentry.won + div.appendChild(newInputCheckbox("Won", subEntry?.won, "subentry-won")); + + // subentry.date + div.appendChild(newInputText("Date", subEntry?.date, "subentry-date")); + + return div; +} + +function getInputValueFromNode(node, name) { + const element = node.getElementsByClassName(name)[0]; + return element.type === "checkbox" ? element.checked : element.value; +} + +function buildRecordObject() { + const node = document.getElementsByClassName("record")[0]; + return { + id: +node.getAttribute("data-id"), + type: getInputValueFromNode(node, "record-type"), + description: getInputValueFromNode(node, "record-description"), + done: getInputValueFromNode(node, "record-done"), + entries: buildEntriesObject(node.getElementsByClassName("record-entries")[0]), + } +} + +function buildEntriesObject(entriesNode) { + const entriesNodes = entriesNode.getElementsByClassName("entry"); + + const result = []; + for (const node of entriesNodes) { + result.push({ + id: +node.getAttribute("data-id"), + bookmaker_id: +getInputValueFromNode(node, "entry-bookmaker_id"), + account_id: +getInputValueFromNode(node, "entry-account_id"), + amount: +getInputValueFromNode(node, "entry-amount"), + sub_entries: buildSubEntriesObject(node.getElementsByClassName("entry-subentries")[0]), + }); + } + return result; +} + +function buildSubEntriesObject(subEntriesNode) { + const subEntriesNodes = subEntriesNode.getElementsByClassName("subentry"); + + const result = []; + for (const node of subEntriesNodes) { + result.push({ + id: +node.getAttribute("data-id"), + description: getInputValueFromNode(node, "subentry-description"), + odds: +getInputValueFromNode(node, "subentry-odds"), + won: getInputValueFromNode(node, "subentry-won"), + date: getInputValueFromNode(node, "subentry-date"), + }); + } + return result; +} + +async function saveRecord() { + const result = await myFetchPOST("/api/records", buildRecordObject()); + if (result) { + alert("Done"); + } +}
M static/js/records.jsstatic/js/records.js

@@ -1,86 +1,17 @@

document.addEventListener('DOMContentLoaded', function () { loadRecords(); - document.getElementById('new-record').addEventListener('click', createNewRecord); + document.getElementById('new-record').addEventListener('click', editRecord); }); -const casino = { - type: 'Arbitraggio', - description: 'Prova', - entries: [ - { - bookmaker_id: 1, - account_id: 1, - amount: 97500, - refund: 0, - bonus: 0, - commission: 0, - sub_entries: [ - { - description: "Punta", - odds: 200, - won: false, - date: new Date().toISOString(), - } - ] - }, - { - bookmaker_id: 2, - account_id: 2, - amount: 100000, - refund: 0, - bonus: 0, - commission: 0, - sub_entries: [ - { - description: "Banca", - odds: 195, - won: true, - date: new Date().toISOString(), - } - ] - }, - ] -}; - -const bank = { - type: 'Bancata', - description: 'Prova', - entries: [ - { - bookmaker_id: 1, - account_id: 1, - amount: 3000, - refund: 0, - bonus: 0, - commission: 0, - sub_entries: [ - { - description: "Punta", - odds: 133, - won: true, - date: new Date().toISOString(), - } - ] - }, - { - bookmaker_id: 3, - account_id: 2, - amount: 3057, - refund: 0, - bonus: 0, - commission: 450, - sub_entries: [ - { - description: "Banca", - odds: 135, - won: false, - date: new Date().toISOString(), - } - ] - }, - ] -}; +function editRecord() { + const out = "/records/edit/"; + const id = this.getAttribute("data-id"); + if (id) { + out += "?id=" + id; + } + location.href = out; +} function loadRecords() { getRecords().then(records => {

@@ -101,6 +32,8 @@ header.appendChild(tr);

for (const record of records) { const tr = document.createElement('tr'); + tr.setAttribute("data-id", record.id); + tr.onclick = editRecord; const fields = [ formatDate(record.created_at),

@@ -120,9 +53,3 @@ table.appendChild(tr);

} }); } - -async function createNewRecord() { - await myFetchPOST("/api/records", casino); - await myFetchPOST("/api/records", bank) - loadRecords(); -}
A static/records/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 - Records</title> + <link rel="stylesheet" href="/css/styles.css"> +</head> + +<body> + <header> + <nav></nav> + </header> + <main> + <h1>Records > Edit</h1> + + <div id="main-container"> + </div> + + <button id="save">Save</button> + </main> + <script src="/js/common.js"></script> + <script src="/js/records-edit.js"></script> +</body> + +</html>