teg/backend/dashboard.go
2026-05-19 18:07:22 +02:00

437 lines
8.5 KiB
Go

// backend/dashboard.go
package main
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"time"
)
type DashboardWidget struct {
ID string `json:"id"`
UserID string `json:"userId"`
Type string `json:"type"`
Title string `json:"title"`
X int `json:"x"`
Y int `json:"y"`
W int `json:"w"`
H int `json:"h"`
Config json.RawMessage `json:"config"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type DashboardWidgetRequest struct {
Type string `json:"type"`
Title string `json:"title"`
X int `json:"x"`
Y int `json:"y"`
W int `json:"w"`
H int `json:"h"`
Config json.RawMessage `json:"config"`
}
type DashboardOperationChange struct {
ID string `json:"id"`
OperationID string `json:"operationId"`
OperationNumber string `json:"operationNumber"`
OperationName string `json:"operationName"`
UserDisplayName string `json:"userDisplayName"`
Action string `json:"action"`
Changes []OperationHistoryChange `json:"changes"`
CreatedAt time.Time `json:"createdAt"`
}
func normalizeDashboardWidgetInput(input *DashboardWidgetRequest) {
input.Type = strings.TrimSpace(input.Type)
input.Title = strings.TrimSpace(input.Title)
if input.Type == "" {
input.Type = "note"
}
if input.Title == "" {
input.Title = "Widget"
}
if input.X < 0 {
input.X = 0
}
if input.Y < 0 {
input.Y = 0
}
if input.W < 2 {
input.W = 2
}
if input.W > 12 {
input.W = 12
}
if input.H < 1 {
input.H = 1
}
if input.H > 8 {
input.H = 8
}
if len(input.Config) == 0 || !json.Valid(input.Config) {
input.Config = json.RawMessage(`{}`)
}
}
func scanDashboardWidget(scan func(dest ...any) error) (DashboardWidget, error) {
var widget DashboardWidget
var configRaw []byte
err := scan(
&widget.ID,
&widget.UserID,
&widget.Type,
&widget.Title,
&widget.X,
&widget.Y,
&widget.W,
&widget.H,
&configRaw,
&widget.CreatedAt,
&widget.UpdatedAt,
)
if len(configRaw) == 0 {
configRaw = []byte(`{}`)
}
widget.Config = json.RawMessage(configRaw)
return widget, err
}
func (s *Server) handleListDashboardWidgets(w http.ResponseWriter, r *http.Request) {
user, ok := userFromContext(r.Context())
if !ok {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
rows, err := s.db.Query(
r.Context(),
`
SELECT
id::TEXT,
user_id::TEXT,
type,
title,
x,
y,
w,
h,
config,
created_at,
updated_at
FROM dashboard_widgets
WHERE user_id = $1
ORDER BY y ASC, x ASC, created_at ASC
`,
user.ID,
)
if err != nil {
writeError(w, http.StatusInternalServerError, "Dashboard konnte nicht geladen werden")
return
}
defer rows.Close()
widgets := []DashboardWidget{}
for rows.Next() {
widget, err := scanDashboardWidget(rows.Scan)
if err != nil {
writeError(w, http.StatusInternalServerError, "Dashboard konnte nicht gelesen werden")
return
}
widgets = append(widgets, widget)
}
if err := rows.Err(); err != nil {
writeError(w, http.StatusInternalServerError, "Dashboard konnte nicht gelesen werden")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"widgets": widgets,
})
}
func (s *Server) handleCreateDashboardWidget(w http.ResponseWriter, r *http.Request) {
user, ok := userFromContext(r.Context())
if !ok {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
var input DashboardWidgetRequest
if err := readJSON(r, &input); err != nil {
writeError(w, http.StatusBadRequest, "Invalid JSON")
return
}
normalizeDashboardWidgetInput(&input)
widget, err := scanDashboardWidget(func(dest ...any) error {
return s.db.QueryRow(
r.Context(),
`
INSERT INTO dashboard_widgets (
user_id,
type,
title,
x,
y,
w,
h,
config
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::JSONB)
RETURNING
id::TEXT,
user_id::TEXT,
type,
title,
x,
y,
w,
h,
config,
created_at,
updated_at
`,
user.ID,
input.Type,
input.Title,
input.X,
input.Y,
input.W,
input.H,
string(input.Config),
).Scan(dest...)
})
if err != nil {
writeError(w, http.StatusInternalServerError, "Widget konnte nicht erstellt werden")
return
}
writeJSON(w, http.StatusCreated, map[string]any{
"widget": widget,
})
}
func (s *Server) handleUpdateDashboardWidget(w http.ResponseWriter, r *http.Request) {
user, ok := userFromContext(r.Context())
if !ok {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
widgetID := strings.TrimSpace(r.PathValue("id"))
if widgetID == "" {
writeError(w, http.StatusBadRequest, "Widget-ID fehlt")
return
}
var input DashboardWidgetRequest
if err := readJSON(r, &input); err != nil {
writeError(w, http.StatusBadRequest, "Invalid JSON")
return
}
normalizeDashboardWidgetInput(&input)
widget, err := scanDashboardWidget(func(dest ...any) error {
return s.db.QueryRow(
r.Context(),
`
UPDATE dashboard_widgets
SET
type = $3,
title = $4,
x = $5,
y = $6,
w = $7,
h = $8,
config = $9::JSONB,
updated_at = now()
WHERE id = $1
AND user_id = $2
RETURNING
id::TEXT,
user_id::TEXT,
type,
title,
x,
y,
w,
h,
config,
created_at,
updated_at
`,
widgetID,
user.ID,
input.Type,
input.Title,
input.X,
input.Y,
input.W,
input.H,
string(input.Config),
).Scan(dest...)
})
if err != nil {
writeError(w, http.StatusNotFound, "Widget wurde nicht gefunden")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"widget": widget,
})
}
func (s *Server) handleDeleteDashboardWidget(w http.ResponseWriter, r *http.Request) {
user, ok := userFromContext(r.Context())
if !ok {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
widgetID := strings.TrimSpace(r.PathValue("id"))
if widgetID == "" {
writeError(w, http.StatusBadRequest, "Widget-ID fehlt")
return
}
commandTag, err := s.db.Exec(
r.Context(),
`
DELETE FROM dashboard_widgets
WHERE id = $1
AND user_id = $2
`,
widgetID,
user.ID,
)
if err != nil {
writeError(w, http.StatusInternalServerError, "Widget konnte nicht gelöscht werden")
return
}
if commandTag.RowsAffected() == 0 {
writeError(w, http.StatusNotFound, "Widget wurde nicht gefunden")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
})
}
func (s *Server) handleDashboardOperationChanges(w http.ResponseWriter, r *http.Request) {
limit := 8
if rawLimit := strings.TrimSpace(r.URL.Query().Get("limit")); rawLimit != "" {
parsedLimit, err := strconv.Atoi(rawLimit)
if err == nil {
limit = parsedLimit
}
}
if limit < 1 {
limit = 1
}
if limit > 50 {
limit = 50
}
rows, err := s.db.Query(
r.Context(),
`
SELECT
h.id::TEXT,
h.operation_id::TEXT,
o.operation_number,
o.operation_name,
COALESCE(NULLIF(u.display_name, ''), NULLIF(u.username, ''), u.email, 'Unbekannt'),
h.action,
h.changes,
h.created_at
FROM operation_history h
INNER JOIN operations o ON o.id = h.operation_id
LEFT JOIN users u ON u.id = h.user_id
WHERE h.action <> 'journal'
ORDER BY h.created_at DESC
LIMIT $1
`,
limit,
)
if err != nil {
writeError(w, http.StatusInternalServerError, "Einsatzänderungen konnten nicht geladen werden")
return
}
defer rows.Close()
changes := []DashboardOperationChange{}
for rows.Next() {
var item DashboardOperationChange
var changesRaw []byte
if err := rows.Scan(
&item.ID,
&item.OperationID,
&item.OperationNumber,
&item.OperationName,
&item.UserDisplayName,
&item.Action,
&changesRaw,
&item.CreatedAt,
); err != nil {
writeError(w, http.StatusInternalServerError, "Einsatzänderungen konnten nicht gelesen werden")
return
}
item.Changes = []OperationHistoryChange{}
if len(changesRaw) > 0 {
if err := json.Unmarshal(changesRaw, &item.Changes); err != nil {
writeError(w, http.StatusInternalServerError, "Einsatzänderungen konnten nicht gelesen werden")
return
}
}
changes = append(changes, item)
}
if err := rows.Err(); err != nil {
writeError(w, http.StatusInternalServerError, "Einsatzänderungen konnten nicht gelesen werden")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"changes": changes,
})
}