wishlist and dashboard
Build and Publish / BuildAndDeployAmd64 (push) Failing after 4s
Build and Publish / BuildAndDeployArm64 (push) Failing after 5s

This commit is contained in:
2026-07-01 13:04:55 +02:00
parent 4a37cb479c
commit 1c97a4bdb0
16 changed files with 792 additions and 340 deletions
+156
View File
@@ -17,6 +17,7 @@ import (
"path/filepath"
"strconv"
"strings"
"sort"
"sync"
"time"
@@ -222,6 +223,7 @@ func main() {
mux.HandleFunc("POST /checkout", s.handleCheckout)
mux.HandleFunc("POST /api/orders/from-checkout", s.handleFromCheckout)
mux.HandleFunc("GET /api/orders", s.handleList)
mux.HandleFunc("GET /api/orders/stats", s.handleStats)
mux.HandleFunc("GET /api/orders/{id}", s.handleGet)
mux.HandleFunc("GET /api/orders/{id}/events", s.handleEvents)
@@ -443,6 +445,160 @@ func (s *server) buildPlaceOrder(id order.OrderId, req *checkoutReq) *messages.P
return po
}
// --- dashboard stats ------------------------------------------------------
type orderAlert struct {
Severity string `json:"severity"` // warn | error
Message string `json:"message"`
OrderId string `json:"orderId,omitempty"`
}
type statusBucket struct {
Status order.Status `json:"status"`
Count int `json:"count"`
Total int64 `json:"total"`
}
type revenueSnapshot struct {
Total int64 `json:"total"`
Captured int64 `json:"captured"`
Refunded int64 `json:"refunded"`
Pending int64 `json:"pending"`
}
type dailyRevenue struct {
Date string `json:"date"` // YYYY-MM-DD
Total int64 `json:"total"`
}
type orderStatsResponse struct {
OrdersTotal int `json:"ordersTotal"`
Revenue revenueSnapshot `json:"revenue"`
ByStatus []statusBucket `json:"byStatus"`
RecentOrders []orderSummary `json:"recentOrders"`
DailyRevenue30 []dailyRevenue `json:"dailyRevenue30,omitempty"`
Alerts []orderAlert `json:"alerts"`
}
// handleStats scans all order logs and returns aggregated dashboard stats.
func (s *server) handleStats(w http.ResponseWriter, r *http.Request) {
matches, _ := filepath.Glob(filepath.Join(s.dataDir, "*.events.log"))
now := time.Now()
var totalOrders int
var totalRevenue, capturedRevenue, refundedRevenue int64
byStatus := map[order.Status]int{}
statusTotal := map[order.Status]int64{} // total amount per status
// Daily revenue for the last 30 days (keys are "2026-06-01" sortable strings).
dailyByDay := map[string]int64{}
for i := 0; i < 30; i++ {
day := now.AddDate(0, 0, -i).Format("2006-01-02")
dailyByDay[day] = 0
}
// Collect all order summaries, then sort by placedAt and take top 10.
var allOrders []orderSummary
var alerts []orderAlert
for _, m := range matches {
base := strings.TrimSuffix(filepath.Base(m), ".events.log")
raw, err := strconv.ParseUint(base, 10, 64)
if err != nil {
continue
}
g, err := s.pool.Get(r.Context(), raw)
if err != nil || g.Status == order.StatusNew {
continue
}
totalOrders++
byStatus[g.Status]++
statusTotal[g.Status] += g.TotalAmount.Int64()
totalRevenue += g.TotalAmount.Int64()
capturedRevenue += g.CapturedAmount.Int64()
refundedRevenue += g.RefundedAmount.Int64()
// Daily revenue — parse placedAt to extract date.
if g.PlacedAt != "" {
if placed, parseErr := time.Parse(time.RFC3339, g.PlacedAt); parseErr == nil {
dayKey := placed.Format("2006-01-02")
if _, ok := dailyByDay[dayKey]; ok {
dailyByDay[dayKey] += g.TotalAmount.Int64()
}
}
}
allOrders = append(allOrders, orderSummary{
OrderId: order.OrderId(raw).String(),
Reference: g.OrderReference,
Status: g.Status,
TotalAmount: g.TotalAmount.Int64(),
Currency: g.Currency,
PlacedAt: g.PlacedAt,
})
// Alerts.
if g.Status == order.StatusPending {
alerts = append(alerts, orderAlert{
Severity: "warn",
Message: "Payment still pending",
OrderId: order.OrderId(raw).String(),
})
}
if g.Status == order.StatusCancelled {
alerts = append(alerts, orderAlert{
Severity: "warn",
Message: "Order was cancelled",
OrderId: order.OrderId(raw).String(),
})
}
}
// Sort ALL orders by placedAt descending, take top 10 as "recent".
sort.Slice(allOrders, func(i, j int) bool {
return allOrders[i].PlacedAt > allOrders[j].PlacedAt
})
recent := allOrders
if len(recent) > 10 {
recent = recent[:10]
}
// Cap alerts
if len(alerts) > 50 {
alerts = alerts[:50]
}
// Build status buckets array with totals.
buckets := make([]statusBucket, 0, len(byStatus))
for st, cnt := range byStatus {
buckets = append(buckets, statusBucket{Status: st, Count: cnt, Total: statusTotal[st]})
}
sort.Slice(buckets, func(i, j int) bool {
return buckets[i].Count > buckets[j].Count
})
// Daily revenue as an ordered slice.
var dailyRev []dailyRevenue
for i := 29; i >= 0; i-- {
day := now.AddDate(0, 0, -i).Format("2006-01-02")
dailyRev = append(dailyRev, dailyRevenue{Date: day, Total: dailyByDay[day]})
}
writeJSON(w, http.StatusOK, orderStatsResponse{
OrdersTotal: totalOrders,
Revenue: revenueSnapshot{
Total: totalRevenue,
Captured: capturedRevenue,
Refunded: refundedRevenue,
Pending: totalRevenue - capturedRevenue - refundedRevenue,
},
ByStatus: buckets,
RecentOrders: recent,
DailyRevenue30: dailyRev,
Alerts: alerts,
})
}
// --- reads ----------------------------------------------------------------
func (s *server) handleGet(w http.ResponseWriter, r *http.Request) {