Dasdhboard und Abrechnung
All checks were successful
release-tag / release-image (push) Successful in 2m18s
All checks were successful
release-tag / release-image (push) Successful in 2m18s
This commit is contained in:
@@ -43,3 +43,12 @@ TRADING_SHARE_TRADE=10
|
||||
TRADING_SHARE_HAULING=5
|
||||
TRADING_SHARE_EVENT=0
|
||||
TRADING_SHARE_INTERN=0
|
||||
|
||||
# Trading settlement DMs tell members where to transfer their org share.
|
||||
TRADING_PAYOUT_RECIPIENT=B1tK1ll3r
|
||||
|
||||
# Optional internal web dashboard. Leave DASHBOARD_ADDR empty to disable.
|
||||
# Example local: 127.0.0.1:8080. Example Docker: 0.0.0.0:8080
|
||||
DASHBOARD_ADDR=0.0.0.0:8080
|
||||
# Optional shared token. Dashboard URL then needs ?token=YourSecretToken or Authorization: Bearer YourSecretToken
|
||||
DASHBOARD_TOKEN=ChangeMePlease
|
||||
93
README.md
93
README.md
@@ -248,3 +248,96 @@ Bei Aufträgen und Lagerbeständen sind jetzt diese Einheiten auswählbar:
|
||||
- `SCU`
|
||||
- `cSCU`
|
||||
- `Stück`
|
||||
|
||||
## Trading-Modul
|
||||
|
||||
Das Trading-Modul ersetzt eine Excel-Erfassung für monatliche Trading-/Hauling-Runs.
|
||||
|
||||
### Neue Konfiguration
|
||||
|
||||
```env
|
||||
DISCORD_TRADING_ROLE_IDS=456789012345678901
|
||||
DISCORD_TRADING_REPORT_CHANNEL_ID=TradingReportChannelIDOptional
|
||||
TRADING_SHIPS=Caterpillar,C2 Hercules,Freelancer MAX,Hull A,Hull C,Taurus
|
||||
TRADING_COMMODITIES=Gold,Beryl,Diamond,Laranite,Agricium,Quantanium,Processed Food,Medical Supplies
|
||||
TRADING_SHARE_TRADE=10
|
||||
TRADING_SHARE_HAULING=5
|
||||
TRADING_SHARE_EVENT=0
|
||||
TRADING_SHARE_INTERN=0
|
||||
```
|
||||
|
||||
Die Prozentwerte können als `10` oder `0.10` angegeben werden. Beide Varianten bedeuten 10%.
|
||||
|
||||
### Neue Commands
|
||||
|
||||
```text
|
||||
/trading_add
|
||||
/trading_liste
|
||||
/trading_delete
|
||||
/trading_report
|
||||
/trading_abrechnung
|
||||
```
|
||||
|
||||
`/trading_add` darf von Mitgliedern mit `DISCORD_TRADING_ROLE_IDS` sowie Team/Admin genutzt werden. Schiff und Ware werden aus den Umgebungsvariablen per Autocomplete vorgeschlagen.
|
||||
|
||||
Gewinnberechnung:
|
||||
|
||||
```text
|
||||
Gewinn = (Verkaufswert - Einkaufswert - Kosten) × Wiederholungen
|
||||
Orga-Anteil = max(0, Gewinn × Prozentwert des Zwecks)
|
||||
Transportierte SCU = SCU × Wiederholungen
|
||||
Kosten gesamt = Kosten × Wiederholungen
|
||||
```
|
||||
|
||||
`/trading_report monat:<1-12> jahr:<YYYY>` darf nur von Admins genutzt werden und postet eine hübsche Embed-Auswertung in `DISCORD_TRADING_REPORT_CHANNEL_ID`.
|
||||
|
||||
`/trading_delete id:<id>` ist ein Admin-Kommando. Einträge werden soft-deleted, damit sie nicht mehr in Listen/Reports auftauchen, aber nachvollziehbar bleiben.
|
||||
|
||||
## Trading-Abrechnung per DM
|
||||
|
||||
Admins können am Monatsende jedem Mitglied mit positivem Orga-Anteil automatisch eine private Abrechnung senden:
|
||||
|
||||
```text
|
||||
/trading_abrechnung monat:<1-12> jahr:<YYYY>
|
||||
```
|
||||
|
||||
Der Bot fasst pro Nutzer zusammen:
|
||||
|
||||
- Anzahl Runs
|
||||
- Gewinn
|
||||
- transportierte SCU
|
||||
- zu überweisender Orga-Anteil
|
||||
- Empfänger aus `TRADING_PAYOUT_RECIPIENT`
|
||||
|
||||
Neue Konfiguration:
|
||||
|
||||
```env
|
||||
TRADING_PAYOUT_RECIPIENT=Jan / Orga-Kasse
|
||||
```
|
||||
|
||||
Mitglieder ohne Orga-Anteil werden übersprungen. Falls ein Mitglied keine DMs zulässt, meldet der Bot diese fehlgeschlagene Zustellung in der Command-Antwort und schreibt Details ins Log.
|
||||
|
||||
## Internes Web-Dashboard
|
||||
|
||||
Optional kann ein kleines Web-Dashboard gestartet werden. Es zeigt:
|
||||
|
||||
- Trading-Monatswerte
|
||||
- Abrechnung pro Nutzer
|
||||
- aktive Aufträge
|
||||
- archivierte Aufträge
|
||||
- Lagerbestände
|
||||
|
||||
Aktivierung in `.env`:
|
||||
|
||||
```env
|
||||
DASHBOARD_ADDR=127.0.0.1:8080
|
||||
DASHBOARD_TOKEN=ChangeMePlease
|
||||
```
|
||||
|
||||
Aufruf:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:8080/?token=ChangeMePlease
|
||||
```
|
||||
|
||||
Für Docker sollte `DASHBOARD_ADDR=0.0.0.0:8080` gesetzt werden. Das Dashboard ist als internes Admin-/Team-Werkzeug gedacht. Stelle es nicht ungeschützt öffentlich ins Internet. Nutze mindestens `DASHBOARD_TOKEN`; für produktiven Betrieb idealerweise zusätzlich Reverse Proxy mit HTTPS und Zugriffsbeschränkung.
|
||||
|
||||
@@ -24,6 +24,9 @@ type Config struct {
|
||||
TradingOrgSharePercent map[string]float64
|
||||
DatabasePath string
|
||||
ThreadDeleteAfter time.Duration
|
||||
DashboardAddr string
|
||||
DashboardToken string
|
||||
TradingPayoutRecipient string
|
||||
}
|
||||
|
||||
func loadConfig() Config {
|
||||
@@ -43,6 +46,9 @@ func loadConfig() Config {
|
||||
TradingOrgSharePercent: tradingShareConfig(),
|
||||
DatabasePath: getenv("DATABASE_PATH", "orders.db"),
|
||||
ThreadDeleteAfter: durationHoursEnv("THREAD_DELETE_AFTER_HOURS", 72),
|
||||
DashboardAddr: getenv("DASHBOARD_ADDR", ""),
|
||||
DashboardToken: os.Getenv("DASHBOARD_TOKEN"),
|
||||
TradingPayoutRecipient: getenv("TRADING_PAYOUT_RECIPIENT", "Orga-Kasse"),
|
||||
}
|
||||
if cfg.Token == "" || cfg.AppID == "" || cfg.InternalOrderChannelID == "" {
|
||||
log.Fatal("Bitte DISCORD_TOKEN, DISCORD_APP_ID und DISCORD_INTERNAL_ORDER_CHANNEL_ID setzen")
|
||||
|
||||
34
db.go
34
db.go
@@ -813,3 +813,37 @@ func sortTradingSummary(sum *TradingSummary) {
|
||||
}
|
||||
sort.Slice(sum.PurposeSummary, func(i, j int) bool { return sum.PurposeSummary[i].Purpose < sum.PurposeSummary[j].Purpose })
|
||||
}
|
||||
|
||||
func (s *Store) SummarizeTradingByUser(month, year int) ([]TradingTraderSummary, error) {
|
||||
runs, err := s.ListTradingRuns(TradingListFilter{Month: month, Year: year, Limit: 100000})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
traderMap := map[string]*TradingTraderSummary{}
|
||||
for _, r := range runs {
|
||||
key := r.UserID
|
||||
if key == "" {
|
||||
key = r.UserName
|
||||
}
|
||||
tr := traderMap[key]
|
||||
if tr == nil {
|
||||
tr = &TradingTraderSummary{UserID: r.UserID, UserName: r.UserName}
|
||||
traderMap[key] = tr
|
||||
}
|
||||
tr.Profit += tradingProfit(r)
|
||||
tr.OrgShare += tradingOrgShare(r)
|
||||
tr.SCU += r.SCU * float64(r.Repetitions)
|
||||
tr.Runs++
|
||||
}
|
||||
out := make([]TradingTraderSummary, 0, len(traderMap))
|
||||
for _, tr := range traderMap {
|
||||
out = append(out, *tr)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].OrgShare == out[j].OrgShare {
|
||||
return strings.ToLower(out[i].UserName) < strings.ToLower(out[j].UserName)
|
||||
}
|
||||
return out[i].OrgShare > out[j].OrgShare
|
||||
})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
18
discord.go
18
discord.go
@@ -185,6 +185,8 @@ func (b *Bot) handleCommand(s *discordgo.Session, i *discordgo.InteractionCreate
|
||||
b.cmdTradingDelete(s, i)
|
||||
case "trading_report":
|
||||
b.cmdTradingReport(s, i)
|
||||
case "trading_abrechnung":
|
||||
b.cmdTradingSettlement(s, i)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -758,15 +760,19 @@ func (b *Bot) notifyParticipants(o *Order, body string) {
|
||||
}
|
||||
}
|
||||
func (b *Bot) dm(userID, body string) {
|
||||
ch, err := b.dg.UserChannelCreate(userID)
|
||||
if err != nil {
|
||||
log.Println("dm channel failed:", err)
|
||||
return
|
||||
}
|
||||
if _, err := b.dg.ChannelMessageSend(ch.ID, body); err != nil {
|
||||
if err := b.dmErr(userID, body); err != nil {
|
||||
log.Println("dm send failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) dmErr(userID, body string) error {
|
||||
ch, err := b.dg.UserChannelCreate(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = b.dg.ChannelMessageSend(ch.ID, body)
|
||||
return err
|
||||
}
|
||||
func (b *Bot) audit(body string) {
|
||||
log.Println("AUDIT", body)
|
||||
if b.cfg.AuditChannelID != "" {
|
||||
|
||||
8
main.go
8
main.go
@@ -1,10 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -21,9 +23,15 @@ func main() {
|
||||
if err := bot.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
dashboard := StartDashboard(cfg, store)
|
||||
log.Println("Bot läuft. Beenden mit Ctrl+C.")
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-stop
|
||||
bot.Stop()
|
||||
if dashboard != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_ = dashboard.Shutdown(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -42,6 +43,10 @@ func tradingCommands() []*discordgo.ApplicationCommand {
|
||||
{Name: "monat", Description: "Monat 1-12", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(1), MaxValue: float64(12)},
|
||||
{Name: "jahr", Description: "Jahr", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(2000), MaxValue: float64(2200)},
|
||||
}},
|
||||
{Name: "trading_abrechnung", Description: "Admin: sendet Mitgliedern ihre monatliche Trading-Abrechnung per DM", Options: []*discordgo.ApplicationCommandOption{
|
||||
{Name: "monat", Description: "Monat 1-12", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(1), MaxValue: float64(12)},
|
||||
{Name: "jahr", Description: "Jahr", Type: discordgo.ApplicationCommandOptionInteger, Required: true, MinValue: ptrFloat(2000), MaxValue: float64(2200)},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,3 +337,64 @@ func optUser(i *discordgo.InteractionCreate, s *discordgo.Session, name string)
|
||||
}
|
||||
|
||||
func roundMoney(v float64) int64 { return int64(math.Round(v)) }
|
||||
|
||||
func (b *Bot) cmdTradingSettlement(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
if !b.isAdmin(i) {
|
||||
b.replyEphemeral(s, i, "Nur Admins dürfen Trading-Abrechnungen versenden.")
|
||||
return
|
||||
}
|
||||
month := int(optInt(i, "monat"))
|
||||
year := int(optInt(i, "jahr"))
|
||||
users, err := b.store.SummarizeTradingByUser(month, year)
|
||||
if err != nil {
|
||||
b.replyEphemeral(s, i, "Abrechnung konnte nicht erstellt werden: "+err.Error())
|
||||
return
|
||||
}
|
||||
if len(users) == 0 {
|
||||
b.replyEphemeral(s, i, "Für diesen Monat gibt es keine Trading-Einträge.")
|
||||
return
|
||||
}
|
||||
|
||||
sent, skipped, failed := 0, 0, 0
|
||||
var failedNames []string
|
||||
for _, u := range users {
|
||||
if u.OrgShare <= 0 {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(u.UserID) == "" {
|
||||
failed++
|
||||
failedNames = append(failedNames, u.UserName)
|
||||
continue
|
||||
}
|
||||
body := b.tradingSettlementText(u, month, year)
|
||||
if err := b.dmErr(u.UserID, body); err != nil {
|
||||
failed++
|
||||
failedNames = append(failedNames, u.UserName)
|
||||
log.Println("trading settlement dm failed:", u.UserName, err)
|
||||
continue
|
||||
}
|
||||
sent++
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("Abrechnung %02d/%d versendet. Erfolgreich: %d, übersprungen ohne Orga-Anteil: %d, fehlgeschlagen: %d.", month, year, sent, skipped, failed)
|
||||
if len(failedNames) > 0 {
|
||||
msg += "\nFehlgeschlagen: " + strings.Join(failedNames, ", ")
|
||||
}
|
||||
b.audit(fmt.Sprintf("Trading-Abrechnung %02d/%d durch %s versendet: %d erfolgreich, %d ohne Anteil, %d fehlgeschlagen", month, year, userString(i), sent, skipped, failed))
|
||||
b.replyEphemeral(s, i, msg)
|
||||
}
|
||||
|
||||
func (b *Bot) tradingSettlementText(u TradingTraderSummary, month, year int) string {
|
||||
return fmt.Sprintf(
|
||||
"Abrechnung Trading %02d/%d\n\nHallo %s,\n\nfür deine erfassten Trading-Runs ergibt sich folgende Abrechnung:\n\nRuns: %d\nGewinn: %s\nTransportierte SCU: %s\nDein Orga-Anteil: %s\n\nBitte überweise den Orga-Anteil an: %s\n\nDanke dir!",
|
||||
month,
|
||||
year,
|
||||
u.UserName,
|
||||
u.Runs,
|
||||
formatAUEC(u.Profit),
|
||||
formatAmount(u.SCU),
|
||||
formatAUEC(u.OrgShare),
|
||||
b.cfg.TradingPayoutRecipient,
|
||||
)
|
||||
}
|
||||
|
||||
170
web_dashboard.go
Normal file
170
web_dashboard.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type dashboardData struct {
|
||||
Now time.Time
|
||||
Month int
|
||||
Year int
|
||||
Orders []Order
|
||||
ArchivedOrders []Order
|
||||
Inventory []InventoryItem
|
||||
TradingSummary *TradingSummary
|
||||
TradingUsers []TradingTraderSummary
|
||||
Token string
|
||||
TradingPayee string
|
||||
Error string
|
||||
}
|
||||
|
||||
func StartDashboard(cfg Config, store *Store) *http.Server {
|
||||
if strings.TrimSpace(cfg.DashboardAddr) == "" {
|
||||
return nil
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
h := &dashboardHandler{cfg: cfg, store: store}
|
||||
mux.HandleFunc("/", h.index)
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("ok")) })
|
||||
srv := &http.Server{Addr: cfg.DashboardAddr, Handler: mux}
|
||||
go func() {
|
||||
log.Println("Web-Dashboard läuft auf", cfg.DashboardAddr)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Println("dashboard failed:", err)
|
||||
}
|
||||
}()
|
||||
return srv
|
||||
}
|
||||
|
||||
type dashboardHandler struct {
|
||||
cfg Config
|
||||
store *Store
|
||||
}
|
||||
|
||||
func (h *dashboardHandler) authorized(r *http.Request) bool {
|
||||
if strings.TrimSpace(h.cfg.DashboardToken) == "" {
|
||||
return true
|
||||
}
|
||||
token := r.URL.Query().Get("token")
|
||||
if token == "" {
|
||||
auth := r.Header.Get("Authorization")
|
||||
token = strings.TrimPrefix(auth, "Bearer ")
|
||||
}
|
||||
return token == h.cfg.DashboardToken
|
||||
}
|
||||
|
||||
func (h *dashboardHandler) index(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.authorized(r) {
|
||||
w.Header().Set("WWW-Authenticate", "Bearer")
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
month := intFromQuery(r, "monat", int(now.Month()))
|
||||
year := intFromQuery(r, "jahr", now.Year())
|
||||
data := dashboardData{Now: now, Month: month, Year: year, Token: r.URL.Query().Get("token"), TradingPayee: h.cfg.TradingPayoutRecipient}
|
||||
var errs []string
|
||||
if orders, err := h.store.ListOrders("", 25); err == nil {
|
||||
data.Orders = orders
|
||||
} else {
|
||||
errs = append(errs, "Aufträge: "+err.Error())
|
||||
}
|
||||
if archived, err := h.store.ListArchivedOrders("", 10); err == nil {
|
||||
data.ArchivedOrders = archived
|
||||
} else {
|
||||
errs = append(errs, "Archiv: "+err.Error())
|
||||
}
|
||||
if inv, err := h.store.ListInventory("", 100); err == nil {
|
||||
data.Inventory = inv
|
||||
} else {
|
||||
errs = append(errs, "Lager: "+err.Error())
|
||||
}
|
||||
if sum, err := h.store.SummarizeTradingRuns(month, year); err == nil {
|
||||
data.TradingSummary = sum
|
||||
} else {
|
||||
errs = append(errs, "Trading: "+err.Error())
|
||||
}
|
||||
if users, err := h.store.SummarizeTradingByUser(month, year); err == nil {
|
||||
data.TradingUsers = users
|
||||
} else {
|
||||
errs = append(errs, "Abrechnung: "+err.Error())
|
||||
}
|
||||
data.Error = strings.Join(errs, " | ")
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := dashboardTemplate.Execute(w, data); err != nil {
|
||||
log.Println("dashboard render failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func intFromQuery(r *http.Request, key string, fallback int) int {
|
||||
v := strings.TrimSpace(r.URL.Query().Get(key))
|
||||
if v == "" {
|
||||
return fallback
|
||||
}
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
var dashboardTemplate = template.Must(template.New("dashboard").Funcs(template.FuncMap{
|
||||
"status": statusLabel,
|
||||
"purpose": tradingPurposeLabel,
|
||||
"amount": formatAmount,
|
||||
"auec": formatAUEC,
|
||||
"profit": tradingProfit,
|
||||
"org": tradingOrgShare,
|
||||
"totalSCU": func(r TradingRun) float64 { return r.SCU * float64(r.Repetitions) },
|
||||
"location": func(v string) string {
|
||||
if strings.TrimSpace(v) == "" {
|
||||
return "-"
|
||||
}
|
||||
return v
|
||||
},
|
||||
"monthName": func(m int) string { return time.Month(m).String() },
|
||||
"formatPercent": func(v float64) string { return fmt.Sprintf("%.1f%%", v*100) },
|
||||
}).Parse(`<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>SF Trading Dashboard</title>
|
||||
<style>
|
||||
:root{--bg:#0f172a;--panel:#111827;--card:#1f2937;--text:#e5e7eb;--muted:#94a3b8;--accent:#38bdf8;--green:#22c55e;--red:#ef4444;--yellow:#f59e0b}*{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,Segoe UI,sans-serif;background:var(--bg);color:var(--text)}header{padding:24px 28px;border-bottom:1px solid #334155;background:#020617;position:sticky;top:0;z-index:1}h1{margin:0;font-size:24px}main{padding:24px;display:grid;gap:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px}.card{background:var(--card);border:1px solid #334155;border-radius:16px;padding:16px;box-shadow:0 10px 24px rgba(0,0,0,.22)}.metric{font-size:28px;font-weight:700}.muted{color:var(--muted)}table{width:100%;border-collapse:collapse;font-size:14px}th,td{padding:9px 8px;border-bottom:1px solid #334155;text-align:left;vertical-align:top}th{color:#cbd5e1;background:#111827}.scroll{overflow:auto}.badge{display:inline-block;border-radius:999px;padding:2px 8px;background:#334155;color:#dbeafe;font-size:12px}.err{background:#7f1d1d;border-color:#fecaca}.filters{display:flex;gap:8px;align-items:end;flex-wrap:wrap}.filters input{background:#020617;color:var(--text);border:1px solid #475569;border-radius:10px;padding:8px}.filters button{background:var(--accent);border:0;border-radius:10px;padding:9px 14px;font-weight:700;color:#00111c}a{color:#7dd3fc}</style>
|
||||
</head>
|
||||
<body>
|
||||
<header><h1>SF Trading Dashboard</h1><div class="muted">Stand: {{.Now.Format "02.01.2006 15:04"}}</div></header>
|
||||
<main>
|
||||
{{if .Error}}<section class="card err"><strong>Hinweis:</strong> {{.Error}}</section>{{end}}
|
||||
<section class="card">
|
||||
<form class="filters" method="get">
|
||||
{{if .Token}}<input type="hidden" name="token" value="{{.Token}}">{{end}}
|
||||
<label>Monat<br><input name="monat" type="number" min="1" max="12" value="{{.Month}}"></label>
|
||||
<label>Jahr<br><input name="jahr" type="number" min="2000" max="2200" value="{{.Year}}"></label>
|
||||
<button type="submit">Aktualisieren</button>
|
||||
<span class="muted">Filter betrifft Trading-Auswertung und Abrechnung.</span>
|
||||
</form>
|
||||
</section>
|
||||
{{with .TradingSummary}}
|
||||
<section class="grid">
|
||||
<div class="card"><div class="muted">Trading-Einträge</div><div class="metric">{{.Count}}</div></div>
|
||||
<div class="card"><div class="muted">Gewinn gesamt</div><div class="metric">{{auec .GrossProfit}}</div></div>
|
||||
<div class="card"><div class="muted">Orga-Anteil</div><div class="metric">{{auec .OrgShare}}</div></div>
|
||||
<div class="card"><div class="muted">Transportierte SCU</div><div class="metric">{{amount .TotalSCU}}</div></div>
|
||||
<div class="card"><div class="muted">Kosten gesamt</div><div class="metric">{{auec .TotalCosts}}</div></div>
|
||||
</section>
|
||||
{{end}}
|
||||
<section class="card"><h2>Abrechnung {{printf "%02d/%d" .Month .Year}}</h2><div class="muted">Empfänger: {{.TradingPayee}}</div><div class="scroll"><table><thead><tr><th>Nutzer</th><th>Runs</th><th>Gewinn</th><th>SCU</th><th>Orga-Anteil</th></tr></thead><tbody>{{range .TradingUsers}}<tr><td>{{.UserName}}</td><td>{{.Runs}}</td><td>{{auec .Profit}}</td><td>{{amount .SCU}}</td><td><strong>{{auec .OrgShare}}</strong></td></tr>{{else}}<tr><td colspan="5" class="muted">Keine Daten</td></tr>{{end}}</tbody></table></div></section>
|
||||
<section class="card"><h2>Aktive Aufträge</h2><div class="scroll"><table><thead><tr><th>ID</th><th>Ware</th><th>Menge</th><th>Frist</th><th>Status</th><th>Einreicher</th></tr></thead><tbody>{{range .Orders}}<tr><td>#{{.ID}}</td><td>{{.Commodity}}</td><td>{{amount .QuantityAmount}} {{.QuantityUnit}}</td><td>{{.Deadline}}</td><td><span class="badge">{{status .Status}}</span></td><td>{{.SubmitterName}}</td></tr>{{else}}<tr><td colspan="6" class="muted">Keine aktiven Aufträge</td></tr>{{end}}</tbody></table></div></section>
|
||||
<section class="card"><h2>Lager</h2><div class="scroll"><table><thead><tr><th>ID</th><th>Ware</th><th>Qualität</th><th>Menge</th><th>Ort</th><th>Notiz</th></tr></thead><tbody>{{range .Inventory}}<tr><td>#{{.ID}}</td><td>{{.Name}}</td><td>{{.Quality}}</td><td>{{amount .QuantityAmount}} {{.QuantityUnit}}</td><td>{{location .Location}}</td><td>{{.Note}}</td></tr>{{else}}<tr><td colspan="6" class="muted">Kein Lagerbestand</td></tr>{{end}}</tbody></table></div></section>
|
||||
<section class="card"><h2>Archivierte Aufträge</h2><div class="scroll"><table><thead><tr><th>ID</th><th>Ware</th><th>Status</th><th>Aktualisiert</th></tr></thead><tbody>{{range .ArchivedOrders}}<tr><td>#{{.ID}}</td><td>{{.Commodity}}</td><td><span class="badge">{{status .Status}}</span></td><td>{{.UpdatedAt.Format "02.01.2006 15:04"}}</td></tr>{{else}}<tr><td colspan="4" class="muted">Keine archivierten Aufträge</td></tr>{{end}}</tbody></table></div></section>
|
||||
</main>
|
||||
</body>
|
||||
</html>`))
|
||||
Reference in New Issue
Block a user