all repos — piggy @ 6eb6efa5eeb457d4ad0b4ce27e48c6b21dc42976

Dead simple finance manager in Go, HTML and JS.

initial commit
Marco Andronaco andronacomarco@gmail.com
Sat, 05 Oct 2024 18:59:27 +0200
commit

6eb6efa5eeb457d4ad0b4ce27e48c6b21dc42976

A .gitignore

@@ -0,0 +1,5 @@

+data +dist +__debug_bin* +tmp +
A .vscode/launch.json

@@ -0,0 +1,15 @@

+{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "." + } + ] +}
A LICENSE

@@ -0,0 +1,21 @@

+The MIT License (MIT) + +Copyright (c) 2024 BiRabittoh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
A go.mod

@@ -0,0 +1,26 @@

+module github.com/BiRabittoh/piggy + +go 1.23.1 + +require ( + gorm.io/driver/sqlite v1.5.6 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/glebarez/sqlite v1.11.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/text v0.14.0 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect +)
A go.sum

@@ -0,0 +1,36 @@

+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= +gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
A main.go

@@ -0,0 +1,11 @@

+package main + +import ( + "github.com/BiRabittoh/piggy/src/api" + "github.com/BiRabittoh/piggy/src/app" +) + +func main() { + app.InitDB() + api.ListenAndServe() +}
A src/api/api.go

@@ -0,0 +1,97 @@

+package api + +import ( + "encoding/json" + "log" + "net/http" + + "github.com/BiRabittoh/piggy/src/app" +) + +func getBookmakers(w http.ResponseWriter, r *http.Request) { + var bookmakers []app.Bookmaker + err := app.DB.Find(&bookmakers).Error + if err != nil { + log.Println("could not get bookmakers: " + err.Error()) + new500Error(w, err) + return + } + + jsonResponse(w, bookmakers) +} + +func postBookmakers(w http.ResponseWriter, r *http.Request) { + var bookmaker app.Bookmaker + err := json.NewDecoder(r.Body).Decode(&bookmaker) + if err != nil { + log.Println("could not decode bookmaker JSON: " + err.Error()) + new400Error(w, err) + return + } + err = app.DB.Save(&bookmaker).Error + if err != nil { + log.Println("could not save bookmaker: " + err.Error()) + new500Error(w, err) + return + } + + jsonResponse(w, bookmaker) +} + +func getAccounts(w http.ResponseWriter, r *http.Request) { + var accounts []app.Account + err := app.DB.Find(&accounts).Error + if err != nil { + log.Println("could not get accounts: " + err.Error()) + new500Error(w, err) + return + } + + jsonResponse(w, accounts) +} + +func postAccounts(w http.ResponseWriter, r *http.Request) { + var account app.Account + err := json.NewDecoder(r.Body).Decode(&account) + if err != nil { + log.Println("could not decode account JSON: " + err.Error()) + new400Error(w, err) + return + } + err = app.DB.Save(&account).Error + if err != nil { + log.Println("could not save account: " + err.Error()) + new500Error(w, err) + return + } + + jsonResponse(w, account) +} + +func getRecords(w http.ResponseWriter, r *http.Request) { + records, _, err := app.GetRecords() + if err != nil { + log.Println("could not get records: " + err.Error()) + new500Error(w, err) + } + + jsonResponse(w, records) +} + +func postRecords(w http.ResponseWriter, r *http.Request) { + var record app.Record + err := json.NewDecoder(r.Body).Decode(&record) + if err != nil { + log.Println("could not decode record JSON: " + err.Error()) + new400Error(w, err) + return + } + err = app.DB.Save(&record).Error + if err != nil { + log.Println("could not save record: " + err.Error()) + new500Error(w, err) + return + } + + jsonResponse(w, record) +}
A src/api/routes.go

@@ -0,0 +1,24 @@

+package api + +import ( + "log" + "net/http" +) + +const address = ":3000" + +func ListenAndServe() { + http.Handle("GET /", http.FileServer(http.Dir("static"))) + + http.HandleFunc("GET /api/bookmakers", getBookmakers) + http.HandleFunc("POST /api/bookmakers", postBookmakers) + + http.HandleFunc("GET /api/accounts", getAccounts) + http.HandleFunc("POST /api/accounts", postAccounts) + + http.HandleFunc("GET /api/records", getRecords) + http.HandleFunc("POST /api/records", postRecords) + + log.Println("Serving at " + address + "...") + log.Fatal(http.ListenAndServe(address, nil)) +}
A src/api/utils.go

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

+package api + +import ( + "encoding/json" + "log" + "net/http" +) + +type APIError struct { + Error string `json:"error"` +} + +func jsonResponse(w http.ResponseWriter, v any) { + w.Header().Add("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(v) + if err != nil { + log.Println("could not encode JSON response: " + err.Error()) + } +} + +func new500Error(w http.ResponseWriter, err error) { + http.Error(w, err.Error(), http.StatusInternalServerError) +} + +func new400Error(w http.ResponseWriter, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) +}
A src/app/init.go

@@ -0,0 +1,62 @@

+package app + +import ( + "log" + "os" + "path" + + "github.com/glebarez/sqlite" + "gorm.io/gorm" +) + +const ( + dbDir = "data" + sqliteSuffix = "?_pragma=foreign_keys(1)" +) + +var DB *gorm.DB + +func InitDB() { + err := os.MkdirAll(dbDir, os.ModePerm) + if err != nil { + log.Println(err) // do not return here + } + + dsn := path.Join(dbDir, "data.sqlite") + sqliteSuffix + + DB, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{}) + if err != nil { + log.Fatal("Failed to connect to database: ", err) + } + + // Migrate schema + err = DB.AutoMigrate(&Bookmaker{}, &Account{}, &Record{}, &Entry{}, &SubEntry{}) + if err != nil { + log.Fatal(err) + } + + var bookmakersAmount int64 + err = DB.Model(&Bookmaker{}).Count(&bookmakersAmount).Error + if err != nil { + log.Println("could not count bookmakers: " + err.Error()) + } + if bookmakersAmount == 0 { + err = DB.Create(&Bookmaker{Name: "Test"}).Error + if err != nil { + log.Fatal(err) + } + err = DB.Create(&Bookmaker{Name: "Test 2"}).Error + if err != nil { + log.Fatal(err) + } + err = DB.Create(&Bookmaker{Name: "Test Exchange", Exchange: true}).Error + if err != nil { + log.Fatal(err) + } + } + + err = DB.Model(&Bookmaker{}).Where(&Bookmaker{Exchange: true}).Pluck("id", &ExchangeIDs).Error + if err != nil { + log.Println("could not get exchange ids: " + err.Error()) + } +}
A src/app/models.go

@@ -0,0 +1,178 @@

+package app + +import ( + "log" + "time" +) + +type Bookmaker struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + + Name string `json:"name" gorm:"not null" ` + Exchange bool `json:"exchange" gorm:"not null" ` + DefaultCommission uint `json:"default_commission" gorm:"not null"` +} + +type Account struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"created_at"` + + Name string `json:"name" gorm:"not null"` +} + +type Record struct { + ID uint `json:"id" gorm:"primaryKey"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + + Done bool `json:"done" gorm:"not null"` + Type string `json:"type" gorm:"not null"` + Description string `json:"description" gorm:"not null"` + + Date *time.Time `json:"date" gorm:"-"` + Value *int `json:"value" gorm:"-"` + + Entries []Entry `json:"entries" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` +} + +type Entry struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + + RecordID uint `json:"record_id" gorm:"not null"` + BookmakerID uint `json:"bookmaker_id" gorm:"not null"` + AccountID uint `json:"account_id" gorm:"not null"` + Amount uint `json:"amount" gorm:"not null"` // In cents (ex: 100 = 1.00) + Refund uint `json:"refund" gorm:"not null"` // In cents (ex: 100 = 1.00) + Bonus uint `json:"bonus" gorm:"not null"` // In cents (ex: 50 = 0.50) + Commission uint `json:"commission" gorm:"not null"` // In cents (ex: 4.5% = 450) + + Odds *uint `json:"odds" gorm:"-"` + Won *bool `json:"won" gorm:"-"` + Date *time.Time `json:"date" gorm:"-"` + Value *int `json:"value" gorm:"-"` + + SubEntries []SubEntry `json:"sub_entries" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` +} + +type SubEntry struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + + EntryID uint `json:"entry_id" gorm:"not null"` + Description string `json:"description" gorm:"not null"` + Odds uint `json:"odds" gorm:"not null"` // In cents (ex: 200 = 2.00) + Won bool `json:"won" gorm:"not null"` + Date time.Time `json:"date" gorm:"not null;default:current_timestamp"` + + Value *int `json:"value" gorm:"-"` +} + +func (e *Entry) GetOdds() *uint { + v := uint(1) + for _, s := range e.SubEntries { + v *= s.Odds + } + return &v +} + +func (e *Entry) DidWin() *bool { + v := true + for _, s := range e.SubEntries { + if !s.Won { + v = false + return &v + } + } + return &v +} + +func (e *Entry) GetDate() *time.Time { + if len(e.SubEntries) == 0 { + return nil + } + + last := e.SubEntries[0].Date + for _, s := range e.SubEntries { + if s.Date.After(last) { + last = s.Date + } + } + return &last +} + +func (e *Entry) GetValue() (value int) { + if e.Won == nil || e.Odds == nil { + log.Fatalf("please, update e.Won and e.Odds first") + } + + if IsExchange(e.BookmakerID) { + r := (int(e.Amount) * (int(*e.Odds) - 100)) / 100 + if *e.Won { + value = int(e.Amount) - int(e.Amount)*int(e.Commission)/10000 + } else { + value = int(e.Refund) - r + } + } else { + if *e.Won { + value = (int(e.Amount) * int(*e.Odds) / 100) - int(e.Amount) + } else { + value = -int(e.Amount) + int(e.Refund) + } + } + + value += int(e.Bonus) + return +} + +func (r *Record) GetDate() *time.Time { + if len(r.Entries) == 0 { + return nil + } + + last := *r.Entries[0].Date + for _, e := range r.Entries { + if e.Date.After(last) { + last = *e.Date + } + } + return &last +} + +func FillEntryValues(entries []Entry) ([]Entry, int) { + var total int + for i := range entries { + entries[i].Odds = entries[i].GetOdds() + entries[i].Won = entries[i].DidWin() + entries[i].Date = entries[i].GetDate() + v := entries[i].GetValue() + entries[i].Value = &v + total += v + } + return entries, total +} + +func FillRecordValues(records []Record) ([]Record, int) { + var total int + for i := range records { + _, v := FillEntryValues(records[i].Entries) + records[i].Date = records[i].GetDate() + records[i].Value = &v + total += v + } + return records, total +} + +func GetRecords() (records []Record, total int, err error) { + err = DB.Preload("Entries.SubEntries").Find(&records).Error + if err != nil { + return + } + + records, total = FillRecordValues(records) + return +}
A src/app/utils.go

@@ -0,0 +1,20 @@

+package app + +var ExchangeIDs []uint + +func IndexOf[T comparable](slice []T, element T) int { + for i, el := range slice { + if el == element { + return i + } + } + return -1 +} + +func Contains[T comparable](slice []T, element T) bool { + return IndexOf(slice, element) != -1 +} + +func IsExchange(bookmakerID uint) bool { + return Contains(ExchangeIDs, bookmakerID) +}
A static/accounts/index.html

@@ -0,0 +1,25 @@

+<!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</h1> + + <div id="main-container"> + Accounts will be shown here. + </div> + </main> + <script src="/js/common.js"></script> +</body> + +</html>
A static/bookmakers/index.html

@@ -0,0 +1,25 @@

+<!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</h1> + + <div id="main-container"> + Bookmakers will be shown here. + </div> + </main> + <script src="/js/common.js"></script> +</body> + +</html>
A static/css/styles.css

@@ -0,0 +1,33 @@

+body { + font-family: Arial, sans-serif; + margin: 20px; + background-color: beige; +} + +nav > a:link, nav > a:visited { + color: blue; + margin-right: 10px; +} + +nav > a:hover, nav > a:active { + text-decoration: dotted; +} + +#main-container { + margin-top: 20px; +} + +thead { + font-weight: bold; +} + +table { + width: 100%; +} + +button { + display: block; + margin: 20px auto; + padding: 10px 20px; + font-size: 16px; +}
A static/index.html

@@ -0,0 +1,25 @@

+<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Piggy</title> + <link rel="stylesheet" href="/css/styles.css"> +</head> + +<body> + <header> + <nav></nav> + </header> + <main> + <h1>Piggy</h1> + + <div id="main-container"> + General stats will be shown here. + </div> + </main> + <script src="/js/common.js"></script> +</body> + +</html>
A static/js/common.js

@@ -0,0 +1,49 @@

+document.addEventListener('DOMContentLoaded', function () { + const navObject = document.getElementsByTagName("nav")[0]; + for (const page of navPages) { + const a = document.createElement("a"); + a.innerText = page.name; + a.href = page.href; + navObject.appendChild(a) + } +}); + +const navPages = [ + { name: "Home", href: "/" }, + { name: "Bookmakers", href: "/bookmakers" }, + { name: "Accounts", href: "/accounts" }, + { name: "Records", href: "/records" }, +]; + +const currency = "€"; +const locale = "it-IT"; + +function formatValue(v) { + return (v / 100).toFixed(2); +} + +function formatCash(v) { + return formatValue(v) + currency; +} + +function formatDate(dateString) { + return (new Date(dateString)).toLocaleString(locale); +} + +function fixDate(date) { + date.toISOString().split('T')[0] +} + +async function myFetch(url) { + res = await fetch(url); + if (!res.ok) { + console.error(res.text()) + return + } + + return await res.json(); +} + +async function getRecords() { + return await myFetch('/api/records'); +}
A static/js/records.js

@@ -0,0 +1,138 @@

+document.addEventListener('DOMContentLoaded', function () { + loadRecords(); + + document.getElementById('new-record').addEventListener('click', function () { + createNewRecord(); + }); +}); + +const casino = { + type: 'Arbitraggio', + description: 'Prova', + date: fixDate(new Date()), + entries: [ + { + bookmaker_id: 1, + account_id: 1, + amount: 97500, + refund: 0, + bonus: 0, + commission: 0, + sub_entries: [ + { + description: "Punta", + odds: 200, + won: false, + } + ] + }, + { + bookmaker_id: 2, + account_id: 2, + amount: 100000, + refund: 0, + bonus: 0, + commission: 0, + sub_entries: [ + { + description: "Banca", + odds: 195, + won: true, + } + ] + }, + ] +}; + +const bank = { + type: 'Bancata', + description: 'Prova', + date: fixDate(new Date()), + entries: [ + { + bookmaker_id: 1, + account_id: 1, + amount: 3000, + refund: 0, + bonus: 0, + commission: 0, + sub_entries: [ + { + description: "Punta", + odds: 133, + won: true, + } + ] + }, + { + bookmaker_id: 3, + account_id: 2, + amount: 3057, + refund: 0, + bonus: 0, + commission: 450, + sub_entries: [ + { + description: "Banca", + odds: 135, + won: false, + } + ] + }, + ] +}; + +function loadRecords() { + getRecords().then(records => { + const header = document.getElementById('records-header'); + const table = document.getElementById('records-table'); + header.innerHTML = ''; + table.innerHTML = ''; + + const tr = document.createElement('tr'); + const headers = ["Created", "Done", "Type", "Description", "Date", "Value"]; + + for (const header of headers) { + const td = document.createElement('td'); + td.innerText = header; + tr.appendChild(td); + } + header.appendChild(tr); + + for (const record of records) { + const tr = document.createElement('tr'); + + const fields = [ + formatDate(record.created_at), + record.done, + record.type, + record.description, + formatDate(record.date), + formatCash(record.value), + ]; + + for (const field of fields) { + const td = document.createElement('td'); + td.innerText = field; + tr.appendChild(td); + } + table.appendChild(tr); + } + }); +} + + + +function createNewRecord() { + const recordStr = JSON.stringify(casino); + console.log(recordStr); + + fetch('/api/records', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: recordStr + }).then(response => response.json()) + .then(() => loadRecords()); +}
A static/records/index.html

@@ -0,0 +1,32 @@

+<!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</h1> + + <div id="main-container"> + <table> + <thead id="records-header"></thead> + <tbody id="records-table"></tbody> + <tfoot></tfoot> + </table> + </div> + + <button id="new-record">New Record</button> + </main> + <script src="/js/common.js"></script> + <script src="/js/records.js"></script> +</body> + +</html>