All checks were successful
release-tag / release-image (push) Successful in 2m53s
959 lines
28 KiB
Go
959 lines
28 KiB
Go
package main
|
||
|
||
import (
|
||
"database/sql"
|
||
"encoding/json"
|
||
"fmt"
|
||
"html/template"
|
||
"log"
|
||
"math"
|
||
"net/http"
|
||
"os"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
_ "modernc.org/sqlite" // statt github.com/mattn/go-sqlite3
|
||
)
|
||
|
||
/* Quellen */
|
||
/* https://starmap.space/api/v3/oc/index.php */
|
||
|
||
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)
|
||
hasimpressum = Enabled("KT_HASIMPRESSUM", false)
|
||
impressum = GetENV("KT_IMPRESSUM", "")
|
||
orte = []string{}
|
||
schiffe = []string{
|
||
"", "100i", "125a", "135c", "Arrow", "Aurora CL", "Aurora ES", "Aurora LN", "Aurora LX", "Aurora MR",
|
||
"Avenger Stalker", "Avenger Titan", "Avenger Titan Renegade", "Avenger Warlock",
|
||
"Blade", "Buccaneer", "C1 Spirit", "C2 Hercules Starlifter", "M2 Hercules Starlifter", "A2 Hercules Starlifter", "C8 Pisces", "C8R Pisces Rescue",
|
||
"C8X Pisces Expedition", "Carrack", "Caterpillar", "Constellation Andromeda",
|
||
"Constellation Aquila", "Constellation Phoenix", "Constellation Taurus", "Corsair",
|
||
"Cutlass Black", "Cutlass Blue", "Cutlass Red", "Cutter", "Defender", "Eclipse",
|
||
"Freelancer", "Freelancer DUR", "Freelancer MAX", "Gladiator", "Gladius", "Glaive",
|
||
"Hammerhead", "Hawk", "Herald", "Hurricane", "Idris-P", "Idris-K", "Idris-M", "Javelin",
|
||
"Khartu-Al", "Kraken", "M50", "Merchantman", "Mercury Star Runner", "Mustang Alpha",
|
||
"Mustang Beta", "Mustang Delta", "Mustang Gamma", "Mustang Omega", "Nomad",
|
||
"Orion", "P-52 Merlin", "P-72 Archimedes", "Prospector", "Prowler", "Prowler Utility", "Raft",
|
||
"Reclaimer", "Redeemer", "Reliant Kore", "Reliant Mako", "Reliant Sen", "Reliant Tana",
|
||
"Retaliator", "Sabre Peregrine", "Starfarer Gemini", "Talon", "Talon Shrike",
|
||
"Terrapin", "Vulture", "Hull-A", "Hull-C", "Zeus ES", "Zeus CL",
|
||
// …weitere Capital- und Concept-Schiffe sind ebenfalls bekannt!
|
||
}
|
||
waren = []string{
|
||
"", "Laranite", "Titanium", "Medical Supplies", "Gold", "Agricium", "Hydrogen", "Hydrogen Fuel", "Nitrogen", "Astatine", "Processed Food", "Scrap",
|
||
"Iodine", "Aluminium", "Copper", "Lithium", "Silicon", "Tungsten", "Aphorite", "Beryl", "Bexalite", "Waste", "Osoian hides", "Borase", "WiDoW",
|
||
"Corundum", "Diamond", "Dolivine", "Hadanite", "Hephaestanite", "Laranite", "Quartz", "Taranite", "Stims", "Carbon", "Slam", "Distilled Spirits",
|
||
}
|
||
)
|
||
|
||
type POI struct {
|
||
ItemID int `json:"item_id"`
|
||
System string `json:"System"`
|
||
Planet string `json:"Planet"`
|
||
PoiName string `json:"PoiName"`
|
||
Type string `json:"Type"`
|
||
Classification string `json:"Classification"`
|
||
Latitude float64 `json:"Latitude"`
|
||
Longitude float64 `json:"Longitude"`
|
||
Longitude360 float64 `json:"Longitude360"`
|
||
Height float64 `json:"Height"`
|
||
XCoord float64 `json:"XCoord"`
|
||
YCoord float64 `json:"YCoord"`
|
||
ZCoord float64 `json:"ZCoord"`
|
||
QTMarker int `json:"QTMarker"`
|
||
NextPOI string `json:"NextPOI"`
|
||
NextQTMarker string `json:"NextQTMarker"`
|
||
Comment string `json:"Comment"`
|
||
Submitted string `json:"Submitted"` // oder time.Time, falls du umwandelst
|
||
Introduced string `json:"Introduced"` // z. B. "Unknown"
|
||
GUID string `json:"GUID"`
|
||
POISize *string `json:"POI_Size"`
|
||
POIType *string `json:"POI_Type"`
|
||
POIEntries string `json:"POI_Entries"`
|
||
POIAccessableFoot string `json:"POI_Accessable_Foot"`
|
||
POIAccessableVehicle string `json:"POI_Accessable_Vehicle"`
|
||
POIAccessableShip string `json:"POI_Accessable_Ship"`
|
||
POIDefenses string `json:"POI_Defenses"`
|
||
POILandingPads string `json:"POI_LandingPads"`
|
||
POIVehiclePads string `json:"POI_VehiclePads"`
|
||
POITerminals string `json:"POI_Terminals"`
|
||
POIMedBay string `json:"POI_MedBay"`
|
||
POIServices string `json:"POI_Services"`
|
||
POIAtmosphere *string `json:"POI_Atmosphere"`
|
||
POINPCs string `json:"POI_NPCs"`
|
||
ZoneArmistice int `json:"Zone_Armistice"`
|
||
ZoneNoFly int `json:"Zone_NoFly"`
|
||
ZoneTrespassing int `json:"Zone_Trespassing"`
|
||
ZoneBiome string `json:"Zone_Biome"`
|
||
ZoneGravitation int `json:"Zone_Gravitation"`
|
||
ZoneTemperatureMin string `json:"Zone_Temperature_Min"`
|
||
ZoneTemperatureMax string `json:"Zone_Temperature_Max"`
|
||
Minerals *string `json:"Minerals"`
|
||
SpecialLoot string `json:"SpecialLoot"`
|
||
Missions string `json:"Missions"`
|
||
}
|
||
|
||
type Entry struct {
|
||
ID int
|
||
Anfangsbestand float64
|
||
Endbestand float64
|
||
Prozentwert float64
|
||
Abgabe float64
|
||
Gesamtwert float64
|
||
Bezahlt bool
|
||
CreatedAt string
|
||
Startort string
|
||
Zielort string
|
||
Schiff string
|
||
Ware string
|
||
Zeitaufwand float64
|
||
}
|
||
|
||
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
|
||
data []byte
|
||
)
|
||
|
||
if productive {
|
||
db, err = sql.Open("sqlite", "/data/data.db")
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
data, err = os.ReadFile("/dynamicsrc/pois.json")
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
} else {
|
||
db, err = sql.Open("sqlite", "./data.db")
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
data, err = os.ReadFile("./dynamicsrc/pois.json")
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
}
|
||
|
||
var pois []POI
|
||
if err := json.Unmarshal(data, &pois); err != nil {
|
||
panic(err)
|
||
}
|
||
orte = append(orte, "")
|
||
for _, poi := range pois {
|
||
if poi.System == "Stanton" || poi.System == "Pyro" {
|
||
formatted := fmt.Sprintf("%s - %s - %s (%s)", poi.System, poi.Planet, poi.PoiName, poi.Type)
|
||
orte = append(orte, formatted)
|
||
}
|
||
}
|
||
//
|
||
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
|
||
startort := r.FormValue("startort")
|
||
zielort := r.FormValue("zielort")
|
||
schiff := r.FormValue("schiff")
|
||
ware := r.Form["ware"]
|
||
wareStr := strings.Join(ware, ", ")
|
||
zeitaufwand, _ := strconv.ParseFloat(r.FormValue("zeitaufwand"), 64)
|
||
|
||
_, err := db.Exec(`INSERT INTO eintraege (anfangsbestand, endbestand, prozentwert, abgabe, created_at, startort, zielort, schiff, ware, zeitaufwand) VALUES (?, ?, ?, ?, datetime('now'), ?, ?, ?, ?, ?)`, anfang, ende, prozent, abgabe, startort, zielort, schiff, wareStr, zeitaufwand)
|
||
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, startort, zielort, schiff, ware, zeitaufwand 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
|
||
var startort sql.NullString
|
||
var zielort sql.NullString
|
||
var schiff sql.NullString
|
||
var ware sql.NullString
|
||
var zeitaufwand sql.NullFloat64
|
||
|
||
err := rows.Scan(&e.ID, &e.Anfangsbestand, &e.Endbestand, &e.Prozentwert, &e.Abgabe, &bezahlt, &createdAt, &startort, &zielort, &schiff, &ware, &zeitaufwand)
|
||
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"
|
||
}
|
||
/**/
|
||
if startort.Valid {
|
||
e.Startort = startort.String
|
||
} else {
|
||
e.Startort = "unbekannt"
|
||
}
|
||
|
||
if zielort.Valid {
|
||
e.Zielort = zielort.String
|
||
} else {
|
||
e.Zielort = "unbekannt"
|
||
}
|
||
|
||
if schiff.Valid {
|
||
e.Schiff = schiff.String
|
||
} else {
|
||
e.Schiff = "unbekannt"
|
||
}
|
||
|
||
if ware.Valid {
|
||
e.Ware = ware.String
|
||
} else {
|
||
e.Ware = "unbekannt"
|
||
}
|
||
|
||
if zeitaufwand.Valid {
|
||
e.Zeitaufwand = zeitaufwand.Float64
|
||
} else {
|
||
e.Zeitaufwand = 0
|
||
}
|
||
|
||
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)
|
||
})
|
||
//test
|
||
// 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
|
||
HasImpressum bool
|
||
Impressum string
|
||
Orte []string
|
||
Schiffe []string
|
||
Waren []string
|
||
}{
|
||
Entries: eintraege,
|
||
Summe: summe,
|
||
OffeneSumme: offeneSumme,
|
||
Abteilungen: abteilungen,
|
||
Monatsstatistik: monatsStat,
|
||
LoggedIn: isAuthenticated(r),
|
||
Member: membername,
|
||
HasImpressum: hasimpressum,
|
||
Impressum: impressum,
|
||
Orte: orte,
|
||
Schiffe: schiffe,
|
||
Waren: waren,
|
||
})
|
||
|
||
})
|
||
|
||
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,
|
||
created_at TEXT,
|
||
startort TEXT,
|
||
zielort TEXT,
|
||
schiff TEXT,
|
||
ware TEXT,
|
||
zeitaufwand INTEGER
|
||
);
|
||
`)
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
|
||
// Ergänze ALTER TABLE nur für Migration bestehender Tabellen
|
||
addColumn := func(column, colType string) {
|
||
_, err := db.Exec(`ALTER TABLE eintraege ADD COLUMN ` + column + ` ` + colType)
|
||
if err != nil && !strings.Contains(err.Error(), "duplicate column") {
|
||
log.Fatalf("Fehler beim Hinzufügen der Spalte %s: %v", column, err)
|
||
}
|
||
}
|
||
|
||
addColumn("bezahlt", "INTEGER DEFAULT 0")
|
||
addColumn("created_at", "TEXT")
|
||
addColumn("startort", "TEXT")
|
||
addColumn("zielort", "TEXT")
|
||
addColumn("schiff", "TEXT")
|
||
addColumn("ware", "TEXT")
|
||
addColumn("zeitaufwand", "INTEGER")
|
||
}
|
||
|
||
const loginForm = `
|
||
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Login</title>
|
||
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
|
||
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
|
||
</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">
|
||
<link href="/static/css/tom-select.default.min.css" rel="stylesheet">
|
||
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
|
||
</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 vom Mitglied der Trading-Staffel ({{.Member}})</h1>
|
||
|
||
<div class="alert alert-info">
|
||
<strong>Folgender Wert wurde erwirtschaftet und wird bald zur Verfügung gestellt:</strong> {{formatNumber .OffeneSumme}} UEC
|
||
</div>
|
||
|
||
<div class="alert alert-success">
|
||
<strong>Summe aller getätigten Abgaben an die Community:</strong> {{formatNumber .Summe}} UEC
|
||
</div>
|
||
|
||
<hr />
|
||
|
||
{{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="1" name="anfangsbestand" class="form-control" required>
|
||
</div>
|
||
<div class="col">
|
||
<label class="form-label">Endbestand</label>
|
||
<input type="number" step="1" 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="15">15%</option>
|
||
<option value="20">20%</option>
|
||
<option value="25">25%</option>
|
||
<option value="30">30%</option>
|
||
<option value="40">40%</option>
|
||
<option value="50">50%</option>
|
||
<option value="75">75%</option>
|
||
<option value="100">100%</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="row mb-3">
|
||
<div class="col">
|
||
<label class="form-label">Startort</label>
|
||
<select id="startort" name="startort" class="form-select">
|
||
{{range .Orte}}
|
||
<option value="{{.}}">{{.}}</option>
|
||
{{end}}
|
||
</select>
|
||
</div>
|
||
<div class="col">
|
||
<label class="form-label">Zielort</label>
|
||
<select id="zielort" name="zielort" class="form-select">
|
||
{{range .Orte}}
|
||
<option value="{{.}}">{{.}}</option>
|
||
{{end}}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row mb-3">
|
||
<div class="col">
|
||
<label class="form-label">Schiff</label>
|
||
<select id="schiff" name="schiff" class="form-select">
|
||
{{range .Schiffe}}
|
||
<option value="{{.}}">{{.}}</option>
|
||
{{end}}
|
||
</select>
|
||
</div>
|
||
<div class="col">
|
||
<label class="form-label">Ware</label>
|
||
<select id="ware" name="ware" class="form-select" multiple>
|
||
{{range .Waren}}
|
||
<option value="{{.}}">{{.}}</option>
|
||
{{end}}
|
||
</select>
|
||
</div>
|
||
<div class="col">
|
||
<label class="form-label">Zeitaufwand (min)</label>
|
||
<input type="number" name="zeitaufwand" class="form-control" min="1" required>
|
||
</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>Datum</th>
|
||
<th>UEC Anfang</th>
|
||
<th>UEC Ende</th>
|
||
<th>UEC Profit</th>
|
||
<th>Prozent</th>
|
||
<th>UEC Abgabe</th>
|
||
<th>Status</th>
|
||
{{if .LoggedIn}}<th>Aktion</th>{{else}}<th>Erweitert</th>{{end}}
|
||
</tr>
|
||
</thead>
|
||
<tbody id="eintragsTabelle">
|
||
{{range .Entries}}
|
||
<tr>
|
||
<td>{{.ID}}</td>
|
||
<td>{{formatDate .CreatedAt}}</td>
|
||
<td>{{formatNumber .Anfangsbestand}}</td>
|
||
<td>{{formatNumber .Endbestand}}</td>
|
||
<td>{{formatNumber .Gesamtwert}}</td>
|
||
<td>{{formatNumber .Prozentwert}}%</td>
|
||
<td>{{formatNumber .Abgabe}}</td>
|
||
<td>
|
||
{{if .Bezahlt}}
|
||
{{if $.LoggedIn}}
|
||
<a href="/unmarkaspaid?id={{.ID}}" class="btn btn-sm btn-outline-warning">Als unverteilt markieren</a>
|
||
{{else}}
|
||
<span class="badge bg-success">✓ verteilt</span>
|
||
{{end}}
|
||
{{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>
|
||
<td>
|
||
<button class="btn btn-sm btn-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#details-{{.ID}}" aria-expanded="false" aria-controls="details-{{.ID}}">
|
||
Details
|
||
</button>
|
||
{{if $.LoggedIn}}
|
||
<a href="/delete?id={{.ID}}" class="btn btn-sm btn-danger" onclick="return confirm('Eintrag wirklich löschen?')">Löschen</a>
|
||
{{end}}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="9" class="p-0 border-0">
|
||
<div class="collapse" id="details-{{.ID}}">
|
||
<div class="bg-light p-3 border-top">
|
||
<strong>Interne Infos (Details):</strong>
|
||
<table class="table table-sm table-bordered mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th>Startort</th>
|
||
<th>Zielort</th>
|
||
<th>Schiff</th>
|
||
<th>Ware</th>
|
||
<th>Zeit (min)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>{{.Startort}}</td>
|
||
<td>{{.Zielort}}</td>
|
||
<td>{{.Schiff}}</td>
|
||
<td>{{.Ware}}</td>
|
||
<td>{{formatNumber .Zeitaufwand}}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{{end}}
|
||
</tbody>
|
||
</table>
|
||
|
||
<hr />
|
||
|
||
<h2 class="mb-3">Auswertungen</h2>
|
||
|
||
<ul class="nav nav-tabs" id="auswertungTabs" role="tablist">
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link active" id="monat-tab" data-bs-toggle="tab" data-bs-target="#monat" type="button" role="tab" aria-controls="monat" aria-selected="true">
|
||
Monatliche Übersicht
|
||
</button>
|
||
</li>
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link" id="abteilung-tab" data-bs-toggle="tab" data-bs-target="#abteilung" type="button" role="tab" aria-controls="abteilung" aria-selected="false">
|
||
Verteilung auf Abteilungen
|
||
</button>
|
||
</li>
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link" id="items-tab" data-bs-toggle="tab" data-bs-target="#items" type="button" role="tab" aria-controls="items" aria-selected="false">
|
||
Gegenwert in Items
|
||
</button>
|
||
</li>
|
||
</ul>
|
||
|
||
<div class="tab-content border border-top-0 p-4 bg-white" id="auswertungTabsContent">
|
||
|
||
<!-- Monatliche Übersicht -->
|
||
<div class="tab-pane fade show active" id="monat" role="tabpanel" aria-labelledby="monat-tab">
|
||
<h5 class="mb-3">Monatliche Übersicht</h5>
|
||
<table class="table table-bordered">
|
||
<thead>
|
||
<tr>
|
||
<th>Monat</th>
|
||
<th>Abgaben verteilt</th>
|
||
<th>Abgaben offen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{{range .Monatsstatistik}}
|
||
<tr>
|
||
<td>{{.Monat}}</td>
|
||
<td>{{formatNumber .Summe}} UEC</td>
|
||
<td>{{formatNumber .SummeOffen}} UEC</td>
|
||
</tr>
|
||
{{end}}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Verteilung auf Abteilungen -->
|
||
<div class="tab-pane fade" id="abteilung" role="tabpanel" aria-labelledby="abteilung-tab">
|
||
<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 endgültige Entscheidung über die Verteilung obliegt der Orga-Leitung.
|
||
</div>
|
||
<h5 class="mb-3">Verteilung auf Abteilungen:</h5>
|
||
<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>
|
||
</div>
|
||
|
||
<!-- Gegenwert in Items -->
|
||
<div class="tab-pane fade" id="items" role="tabpanel" aria-labelledby="items-tab">
|
||
<h5 class="mb-3">Gegenwert in Items:</h5>
|
||
<table class="table table-bordered">
|
||
<thead>
|
||
<tr>
|
||
<th>Abteilung</th>
|
||
<th>Beispiel</th>
|
||
<th>Wert pro Item</th>
|
||
<th>Summe verteilt</th>
|
||
<th>Menge</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{{range .Abteilungen}}
|
||
<tr>
|
||
<td>{{.Name}}</td>
|
||
<td>{{.Beispiel}}</td>
|
||
<td>{{formatNumber .WertItem}} UEC</td>
|
||
<td>{{formatNumber .Wert}} UEC</td>
|
||
<td>
|
||
{{if gt .WertItem 0.0}}
|
||
{{formatNumber (div .Wert .WertItem)}}
|
||
{{else}}
|
||
-
|
||
{{end}}
|
||
</td>
|
||
</tr>
|
||
{{end}}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
{{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}}
|
||
<hr />
|
||
{{if .HasImpressum}}
|
||
<div class="alert alert-light">
|
||
<strong><a href="{{.Impressum}}">Impressum</a></strong>
|
||
</div>
|
||
{{end}}
|
||
<hr />
|
||
</div>
|
||
<script src="/static/js/bootstrap.bundle.min.js"></script>
|
||
<script src="/static/js/tom-select.complete.min.js"></script>
|
||
|
||
<script>
|
||
new TomSelect("#zielort", {
|
||
create: true, // erlaubt Freitext
|
||
sortField: "text"
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
new TomSelect("#startort", {
|
||
create: true, // erlaubt Freitext
|
||
sortField: "text"
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
new TomSelect("#schiff", {
|
||
create: true, // erlaubt Freitext
|
||
sortField: "text"
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
new TomSelect("#ware", {
|
||
create: true, // erlaubt Freitext
|
||
sortField: "text",
|
||
plugins: ['remove_button'] // ← erlaubt Entfernen per „x“-Button
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
document.addEventListener("DOMContentLoaded", function () {
|
||
const tabKey = "lastActiveTab";
|
||
|
||
// Tabs initialisieren - sicherstellen, dass Bootstrap geladen ist
|
||
const triggerElList = [].slice.call(document.querySelectorAll('#auswertungTabs button[data-bs-toggle="tab"]'));
|
||
const tabList = triggerElList.map(function (triggerEl) {
|
||
return new bootstrap.Tab(triggerEl);
|
||
});
|
||
|
||
// Falls gespeicherter Tab vorhanden ist, anzeigen
|
||
const lastTabId = localStorage.getItem(tabKey);
|
||
if (lastTabId) {
|
||
const selector = '#auswertungTabs button[data-bs-target="' + lastTabId + '"]';
|
||
const lastTabTrigger = document.querySelector(selector);
|
||
if (lastTabTrigger) {
|
||
new bootstrap.Tab(lastTabTrigger).show();
|
||
}
|
||
}
|
||
|
||
// Tab-Wechsel speichern
|
||
triggerElList.forEach(function (triggerEl) {
|
||
triggerEl.addEventListener("shown.bs.tab", function (event) {
|
||
const target = event.target.getAttribute("data-bs-target");
|
||
localStorage.setItem(tabKey, target);
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|
||
`
|