This commit is contained in:
451
main.go
Normal file
451
main.go
Normal file
@@ -0,0 +1,451 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
_ "modernc.org/sqlite" // statt github.com/mattn/go-sqlite3
|
||||
)
|
||||
|
||||
func GetENV(k, d string) string {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
return v
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func Enabled(k string, def bool) bool {
|
||||
b, err := strconv.ParseBool(strings.ToLower(os.Getenv(k)))
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
var (
|
||||
username = GetENV("KT_USERNAME", "root")
|
||||
password = GetENV("KT_PASSWORD", "root")
|
||||
membername = GetENV("KT_MEMBER", "guest")
|
||||
)
|
||||
|
||||
type Entry struct {
|
||||
ID int
|
||||
Anfangsbestand float64
|
||||
Endbestand float64
|
||||
Prozentwert float64
|
||||
Abgabe float64
|
||||
Gesamtwert float64
|
||||
Bezahlt bool
|
||||
}
|
||||
|
||||
type Abteilung struct {
|
||||
Name string
|
||||
Anteil float64 // in Prozent
|
||||
Wert float64 // berechnet: Anteil * Summe / 100
|
||||
WertOffen float64 // berechnet: Anteil * Summe / 100 (von offen)
|
||||
}
|
||||
|
||||
var tmpl = template.Must(template.New("form").Funcs(template.FuncMap{
|
||||
"formatNumber": formatNumber,
|
||||
}).Parse(htmlTemplate))
|
||||
|
||||
// Tausendertrenner für deutsche Zahlendarstellung (z. B. 12345 → "12.345")
|
||||
func formatNumber(n float64) string {
|
||||
intVal := int64(n + 0.5) // runden
|
||||
s := strconv.FormatInt(intVal, 10)
|
||||
nStr := ""
|
||||
for i, r := range reverse(s) {
|
||||
if i > 0 && i%3 == 0 {
|
||||
nStr = "." + nStr
|
||||
}
|
||||
nStr = string(r) + nStr
|
||||
}
|
||||
return nStr
|
||||
}
|
||||
|
||||
func reverse(s string) string {
|
||||
r := []rune(s)
|
||||
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
|
||||
r[i], r[j] = r[j], r[i]
|
||||
}
|
||||
return string(r)
|
||||
}
|
||||
|
||||
func isAuthenticated(r *http.Request) bool {
|
||||
cookie, err := r.Cookie("session")
|
||||
return err == nil && cookie.Value == "authenticated"
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, err := sql.Open("sqlite", "data.db")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
createTable(db)
|
||||
|
||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||||
|
||||
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
r.ParseForm()
|
||||
user := r.FormValue("username")
|
||||
pass := r.FormValue("password")
|
||||
if user == username && pass == password {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "session",
|
||||
Value: "authenticated",
|
||||
Path: "/",
|
||||
})
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.Error(w, "Login fehlgeschlagen", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(loginForm))
|
||||
})
|
||||
|
||||
http.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "session",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
})
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
})
|
||||
|
||||
http.HandleFunc("/delete", func(w http.ResponseWriter, r *http.Request) {
|
||||
if !isAuthenticated(r) {
|
||||
http.Error(w, "Nicht autorisiert", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
id := r.URL.Query().Get("id")
|
||||
if id != "" {
|
||||
db.Exec("DELETE FROM eintraege WHERE id = ?", id)
|
||||
}
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
})
|
||||
|
||||
http.HandleFunc("/reset", func(w http.ResponseWriter, r *http.Request) {
|
||||
if !isAuthenticated(r) {
|
||||
http.Error(w, "Nicht autorisiert", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
// Tabelle leeren
|
||||
db.Exec("DELETE FROM eintraege")
|
||||
|
||||
// Auto-Increment-Zähler zurücksetzen
|
||||
db.Exec("DELETE FROM sqlite_sequence WHERE name='eintraege'")
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
})
|
||||
|
||||
http.HandleFunc("/markaspaid", func(w http.ResponseWriter, r *http.Request) {
|
||||
if !isAuthenticated(r) {
|
||||
http.Error(w, "Nicht autorisiert", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
id := r.URL.Query().Get("id")
|
||||
if id != "" {
|
||||
db.Exec("UPDATE eintraege SET bezahlt = 1 WHERE id = ?", id)
|
||||
}
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
})
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
if !isAuthenticated(r) {
|
||||
http.Error(w, "Nicht autorisiert", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
r.ParseForm()
|
||||
anfang, _ := strconv.ParseFloat(r.FormValue("anfangsbestand"), 64)
|
||||
ende, _ := strconv.ParseFloat(r.FormValue("endbestand"), 64)
|
||||
prozent, _ := strconv.ParseFloat(r.FormValue("prozentwert"), 64)
|
||||
diff := ende - anfang
|
||||
abgabe := (diff / 100) * prozent
|
||||
|
||||
_, err := db.Exec(`INSERT INTO eintraege (anfangsbestand, endbestand, prozentwert, abgabe) VALUES (?, ?, ?, ?)`,
|
||||
anfang, ende, prozent, abgabe)
|
||||
if err != nil {
|
||||
http.Error(w, "Fehler beim Einfügen", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := db.Query(`SELECT id, anfangsbestand, endbestand, prozentwert, abgabe, bezahlt FROM eintraege`)
|
||||
if err != nil {
|
||||
http.Error(w, "Fehler beim Abrufen", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var eintraege []Entry
|
||||
var summe float64
|
||||
var offeneSumme float64
|
||||
for rows.Next() {
|
||||
var e Entry
|
||||
var bezahlt int
|
||||
err := rows.Scan(&e.ID, &e.Anfangsbestand, &e.Endbestand, &e.Prozentwert, &e.Abgabe, &bezahlt)
|
||||
if err != nil {
|
||||
log.Println("Fehler beim Scan:", err)
|
||||
continue
|
||||
}
|
||||
e.Gesamtwert = e.Endbestand - e.Anfangsbestand
|
||||
e.Bezahlt = bezahlt == 1
|
||||
eintraege = append(eintraege, e)
|
||||
|
||||
if !e.Bezahlt {
|
||||
offeneSumme += e.Abgabe
|
||||
} else {
|
||||
summe += e.Abgabe
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Dynamische Abteilungen – frei anpassbar
|
||||
abteilungen := []Abteilung{
|
||||
{Name: "Raumkampf", Anteil: 15},
|
||||
{Name: "Bodenkampf", Anteil: 8},
|
||||
{Name: "Racing", Anteil: 3},
|
||||
{Name: "Medical", Anteil: 5},
|
||||
{Name: "Exploration", Anteil: 3},
|
||||
{Name: "Rettung", Anteil: 5},
|
||||
{Name: "Logistik", Anteil: 8},
|
||||
{Name: "Mining", Anteil: 3},
|
||||
{Name: "Salvaging", Anteil: 3},
|
||||
{Name: "Trading", Anteil: 3},
|
||||
{Name: "Basebuilding", Anteil: 10},
|
||||
{Name: "Crafting", Anteil: 8},
|
||||
{Name: "Forschung", Anteil: 5},
|
||||
{Name: "Events", Anteil: 15},
|
||||
{Name: "Roleplay", Anteil: 3},
|
||||
{Name: "Kunstflug", Anteil: 3},
|
||||
}
|
||||
|
||||
for i := range abteilungen {
|
||||
abteilungen[i].Wert = (abteilungen[i].Anteil / 100) * summe
|
||||
abteilungen[i].WertOffen = (abteilungen[i].Anteil / 100) * offeneSumme
|
||||
}
|
||||
|
||||
tmpl.Execute(w, struct {
|
||||
Entries []Entry
|
||||
Summe float64
|
||||
OffeneSumme float64
|
||||
Abteilungen []Abteilung
|
||||
LoggedIn bool
|
||||
Member string
|
||||
}{
|
||||
Entries: eintraege,
|
||||
Summe: summe,
|
||||
OffeneSumme: offeneSumme,
|
||||
Abteilungen: abteilungen,
|
||||
LoggedIn: isAuthenticated(r),
|
||||
Member: membername,
|
||||
})
|
||||
})
|
||||
|
||||
log.Println("Server läuft auf http://localhost:8080")
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
||||
|
||||
func createTable(db *sql.DB) {
|
||||
_, err := db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS eintraege (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
anfangsbestand REAL,
|
||||
endbestand REAL,
|
||||
prozentwert REAL,
|
||||
abgabe REAL,
|
||||
bezahlt INTEGER DEFAULT 0
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Falls die Tabelle schon existiert, aber die Spalte "bezahlt" fehlt (z. B. nach Update)
|
||||
_, err = db.Exec(`ALTER TABLE eintraege ADD COLUMN bezahlt INTEGER DEFAULT 0;`)
|
||||
if err != nil && !strings.Contains(err.Error(), "duplicate column") {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
const loginForm = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Login</title>
|
||||
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container mt-5">
|
||||
<h2>Login</h2>
|
||||
<form method="POST" class="card p-4 shadow-sm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Benutzername</label>
|
||||
<input type="text" name="username" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Passwort</label>
|
||||
<input type="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
const htmlTemplate = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Abgabe-Berechnung</title>
|
||||
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container mt-5">
|
||||
<div class="d-flex justify-content-end">
|
||||
{{if .LoggedIn}}
|
||||
<form action="/logout" method="POST">
|
||||
<button type="submit" class="btn btn-sm btn-secondary">Logout</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<a href="/login" class="btn btn-sm btn-outline-primary">Login</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<h1 class="mb-4">Beitrag zur Community von der Trading-Staffel ({{.Member}})</h1>
|
||||
{{if .LoggedIn}}
|
||||
<form method="POST" class="card p-4 mb-4 shadow-sm">
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<label class="form-label">Anfangsbestand</label>
|
||||
<input type="number" step="0.01" name="anfangsbestand" class="form-control" required>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Endbestand</label>
|
||||
<input type="number" step="0.01" name="endbestand" class="form-control" required>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Prozentwert</label>
|
||||
<select name="prozentwert" class="form-select">
|
||||
<option value="30">30%</option>
|
||||
<option value="10">10%</option>
|
||||
<option value="20">20%</option>
|
||||
<option value="30">30%</option>
|
||||
<option value="40">40%</option>
|
||||
<option value="50">50%</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Berechnen & Speichern</button>
|
||||
</form>
|
||||
{{end}}
|
||||
|
||||
<h2 class="mb-3">Gespeicherte Einträge</h2>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Anfang</th>
|
||||
<th>Ende</th>
|
||||
<th>Profit</th>
|
||||
<th>Prozent</th>
|
||||
<th>Abgabe</th>
|
||||
<th>Status</th>
|
||||
{{if .LoggedIn}}<th>Aktion</th>{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Entries}}
|
||||
<tr>
|
||||
<td>{{.ID}}</td>
|
||||
<td>{{formatNumber .Anfangsbestand}}</td>
|
||||
<td>{{formatNumber .Endbestand}}</td>
|
||||
<td>{{formatNumber .Gesamtwert}}</td>
|
||||
<td>{{formatNumber .Prozentwert}}%</td>
|
||||
<td>
|
||||
{{formatNumber .Abgabe}}
|
||||
</td>
|
||||
<td>
|
||||
{{if .Bezahlt}}
|
||||
<span class="badge bg-success">✓ verteilt</span>
|
||||
{{else}}
|
||||
{{if $.LoggedIn}}
|
||||
<a href="/markaspaid?id={{.ID}}" class="btn btn-sm btn-outline-success">Als verteilt markieren</a>
|
||||
{{else}}
|
||||
<span class="badge bg-danger">✗ nicht verteilt</span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</td>
|
||||
{{if $.LoggedIn}}
|
||||
<td>
|
||||
<a href="/delete?id={{.ID}}" class="btn btn-sm btn-danger" onclick="return confirm('Eintrag wirklich löschen?')">Löschen</a>
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<strong>Offen zur Abgabe:</strong> {{formatNumber .OffeneSumme}} UEC
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success">
|
||||
<strong>Summe aller Abgaben an die Knebel-Community:</strong> {{formatNumber .Summe}} UEC
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>Die tatsächlichen Werte können abweichen.</strong> Die dargestellten Werte sind meine Vorstellung einer sinnvollen Verteilung.<br>
|
||||
Die Summe wird an die Orga-Leitung entrichtet. Die entgültige Entscheidung über die Verteilung obliegt der Orga-Leitung.
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4">Verteilung auf Abteilungen:</h4>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Abteilung</th>
|
||||
<th>Verteilungsschlüssel</th>
|
||||
<th>Summe verteilt</th>
|
||||
<th>Summe offen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Abteilungen}}
|
||||
<tr>
|
||||
<td>{{.Name}}</td>
|
||||
<td>{{formatNumber .Anteil}}%</td>
|
||||
<td>{{formatNumber .Wert}} UEC</td>
|
||||
<td>{{formatNumber .WertOffen}} UEC</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{if .LoggedIn}}
|
||||
<form action="/reset" method="POST" onsubmit="return confirm('Alle Einträge wirklich löschen?')">
|
||||
<button type="submit" class="btn btn-outline-danger mt-3">Alle Einträge löschen</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
Reference in New Issue
Block a user