package main import ( "database/sql" "html/template" "log" "math" "net/http" "os" "sort" "strconv" "strings" "time" _ "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", "demo") productive = Enabled("KT_PRODUCTIVE", false) ) type Entry struct { ID int Anfangsbestand float64 Endbestand float64 Prozentwert float64 Abgabe float64 Gesamtwert float64 Bezahlt bool CreatedAt string } type Abteilung struct { Name string Anteil float64 // in Prozent Wert float64 // berechnet: Anteil * Summe / 100 WertOffen float64 // berechnet: Anteil * Summe / 100 (von offen) Beispiel string WertItem float64 } type Monatsstatistik struct { Monat string // z. B. "07.2025" Summe float64 // bezahlte SummeOffen float64 // noch nicht bezahlt } var tmpl = template.Must(template.New("form").Funcs(template.FuncMap{ "formatNumber": formatNumber, "div": func(a, b float64) float64 { if b == 0 { return 0 } return math.Floor(a / b) }, "formatDate": func(dateStr string) string { t, err := time.Parse("2006-01-02 15:04:05", dateStr) if err != nil { return "?" } return t.Format("02.01.2006") }, }).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() { var ( db *sql.DB err error ) if productive { db, err = sql.Open("sqlite", "/data/data.db") } else { db, err = sql.Open("sqlite", "./data.db") } // if err != nil { log.Fatal(err) } defer db.Close() createTable(db) if productive { http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("/data/static")))) } else { 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("/unmarkaspaid", 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 = 0 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, created_at) VALUES (?, ?, ?, ?, datetime('now'))`, 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, created_at 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 var createdAt sql.NullString err := rows.Scan(&e.ID, &e.Anfangsbestand, &e.Endbestand, &e.Prozentwert, &e.Abgabe, &bezahlt, &createdAt) if err != nil { log.Println("Fehler beim Scan:", err) continue } e.Gesamtwert = e.Endbestand - e.Anfangsbestand e.Bezahlt = bezahlt == 1 if !e.Bezahlt { offeneSumme += e.Abgabe } else { summe += e.Abgabe } if createdAt.Valid { e.CreatedAt = createdAt.String } else { e.CreatedAt = "unbekannt" } eintraege = append(eintraege, e) } monatsMap := map[string]*Monatsstatistik{} for _, e := range eintraege { parsed, err := time.Parse("2006-01-02 15:04:05", e.CreatedAt) if err != nil { continue } monatKey := parsed.Format("01.2006") // z. B. "07.2025" if _, ok := monatsMap[monatKey]; !ok { monatsMap[monatKey] = &Monatsstatistik{Monat: monatKey} } if e.Bezahlt { monatsMap[monatKey].Summe += e.Abgabe } else { monatsMap[monatKey].SummeOffen += e.Abgabe } } var monatsStat []Monatsstatistik for _, stat := range monatsMap { monatsStat = append(monatsStat, *stat) } sort.Slice(monatsStat, func(i, j int) bool { // Nach Datum aufsteigend sortieren ti, _ := time.Parse("01.2006", monatsStat[i].Monat) tj, _ := time.Parse("01.2006", monatsStat[j].Monat) return ti.Before(tj) }) // Dynamische Abteilungen – frei anpassbar abteilungen := []Abteilung{ {Name: "Raumkampf", Anteil: 15, Beispiel: "CF-337 Panther", WertItem: 36308}, {Name: "Bodenkampf", Anteil: 8, Beispiel: "P4-AR Rifle", WertItem: 5900}, {Name: "Racing", Anteil: 3, Beispiel: "LumaCore - Power Plant", WertItem: 69300}, {Name: "Medical", Anteil: 5, Beispiel: "ParaMed Medical Device", WertItem: 1250}, {Name: "Exploration", Anteil: 3, Beispiel: "Pembroke Exploration Suit", WertItem: 15000}, {Name: "Rettung", Anteil: 5, Beispiel: "GSX-HP Fuel-Pod", WertItem: 115200}, {Name: "Logistik", Anteil: 8, Beispiel: "MaxLift Tractor Beam", WertItem: 19175}, {Name: "Mining", Anteil: 3, Beispiel: "Helix II", WertItem: 108000}, {Name: "Salvaging", Anteil: 3, Beispiel: "Abrade Scraper Module", WertItem: 21250}, {Name: "Trading", Anteil: 3, Beispiel: "MaxLift Tractor Beam", WertItem: 19175}, {Name: "Basebuilding (+10)", Anteil: 0, Beispiel: "CF-337 Panther", WertItem: 36308}, {Name: "Crafting (+8)", Anteil: 0, Beispiel: "CF-337 Panther", WertItem: 36308}, {Name: "Forschung (+5)", Anteil: 0, Beispiel: "CF-337 Panther", WertItem: 36308}, {Name: "Events (-23)", Anteil: 38, Beispiel: "CF-337 Panther", WertItem: 36308}, {Name: "Roleplay", Anteil: 3, Beispiel: "Clothing", WertItem: 8400}, {Name: "Kunstflug", Anteil: 3, Beispiel: "Beacon Undersuit Crimson", WertItem: 1000}, } 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 Monatsstatistik []Monatsstatistik LoggedIn bool Member string }{ Entries: eintraege, Summe: summe, OffeneSumme: offeneSumme, Abteilungen: abteilungen, Monatsstatistik: monatsStat, LoggedIn: isAuthenticated(r), Member: membername, }) }) log.Println("Server läuft auf http://0.0.0.0: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) } _, err = db.Exec(`ALTER TABLE eintraege ADD COLUMN created_at TEXT`) if err != nil && !strings.Contains(err.Error(), "duplicate column") { log.Fatal(err) } } const loginForm = ` Login

Login

` const htmlTemplate = ` Abgabe-Berechnung
{{if .LoggedIn}}
{{else}} Login {{end}}

Beitrag zur Community vom Mitglied der Trading-Staffel ({{.Member}})

Folgender Wert wurde erwirtschaftet und wird bald zur Verfügung gestellt: {{formatNumber .OffeneSumme}} UEC
Summe aller getätigten Abgaben an die Community: {{formatNumber .Summe}} UEC

{{if .LoggedIn}}
{{end}}

Gespeicherte Einträge

{{if .LoggedIn}}{{end}} {{range .Entries}} {{if $.LoggedIn}} {{end}} {{end}}
# Datum Anfang Ende Profit Prozent Abgabe StatusAktion
{{.ID}} {{formatDate .CreatedAt}} {{formatNumber .Anfangsbestand}} {{formatNumber .Endbestand}} {{formatNumber .Gesamtwert}} {{formatNumber .Prozentwert}}% {{formatNumber .Abgabe}} {{if .Bezahlt}} {{if $.LoggedIn}} Als unverteilt markieren {{else}} ✓ verteilt {{end}} {{else}} {{if $.LoggedIn}} Als verteilt markieren {{else}} ✗ nicht verteilt {{end}} {{end}} Löschen

Auswertungen

Monatliche Übersicht
{{range .Monatsstatistik}} {{end}}
Monat Abgaben verteilt Abgaben offen
{{.Monat}} {{formatNumber .Summe}} UEC {{formatNumber .SummeOffen}} UEC
Die tatsächlichen Werte können abweichen. Die dargestellten Werte sind meine Vorstellung einer sinnvollen Verteilung.
Die Summe wird an die Orga-Leitung entrichtet. Die endgültige Entscheidung über die Verteilung obliegt der Orga-Leitung.
Verteilung auf Abteilungen:
{{range .Abteilungen}} {{end}}
Abteilung Verteilungsschlüssel Summe verteilt Summe offen
{{.Name}} {{formatNumber .Anteil}}% {{formatNumber .Wert}} UEC {{formatNumber .WertOffen}} UEC
Gegenwert in Items:
{{range .Abteilungen}} {{end}}
Abteilung Beispiel Wert pro Item Summe verteilt Menge
{{.Name}} {{.Beispiel}} {{formatNumber .WertItem}} UEC {{formatNumber .Wert}} UEC {{if gt .WertItem 0.0}} {{formatNumber (div .Wert .WertItem)}} {{else}} - {{end}}
{{if .LoggedIn}}
{{end}}
`