565 lines
11 KiB
Go
565 lines
11 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"`
|
|
}
|
|
|
|
type DashboardSettings struct {
|
|
UserID string `json:"userId"`
|
|
Settings json.RawMessage `json:"settings"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
type DashboardSettingsRequest struct {
|
|
Settings json.RawMessage `json:"settings"`
|
|
}
|
|
|
|
func scanDashboardSettings(scan func(dest ...any) error) (DashboardSettings, error) {
|
|
var settings DashboardSettings
|
|
var settingsRaw []byte
|
|
|
|
err := scan(
|
|
&settings.UserID,
|
|
&settingsRaw,
|
|
&settings.CreatedAt,
|
|
&settings.UpdatedAt,
|
|
)
|
|
|
|
if len(settingsRaw) == 0 {
|
|
settingsRaw = []byte(`{}`)
|
|
}
|
|
|
|
settings.Settings = json.RawMessage(settingsRaw)
|
|
|
|
return settings, err
|
|
}
|
|
|
|
func (s *Server) handleGetDashboardSettings(w http.ResponseWriter, r *http.Request) {
|
|
user, ok := userFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
|
return
|
|
}
|
|
|
|
settings, err := scanDashboardSettings(func(dest ...any) error {
|
|
return s.db.QueryRow(
|
|
r.Context(),
|
|
`
|
|
INSERT INTO dashboard_settings (
|
|
user_id,
|
|
settings
|
|
)
|
|
VALUES (
|
|
$1,
|
|
'{}'::JSONB
|
|
)
|
|
ON CONFLICT (user_id)
|
|
DO UPDATE SET
|
|
user_id = EXCLUDED.user_id
|
|
RETURNING
|
|
user_id::TEXT,
|
|
settings,
|
|
created_at,
|
|
updated_at
|
|
`,
|
|
user.ID,
|
|
).Scan(dest...)
|
|
})
|
|
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Dashboard-Einstellungen konnten nicht geladen werden")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"dashboardSettings": settings,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleUpdateDashboardSettings(w http.ResponseWriter, r *http.Request) {
|
|
user, ok := userFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
|
return
|
|
}
|
|
|
|
var input DashboardSettingsRequest
|
|
|
|
if err := readJSON(r, &input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Invalid JSON")
|
|
return
|
|
}
|
|
|
|
if len(input.Settings) == 0 || !json.Valid(input.Settings) {
|
|
input.Settings = json.RawMessage(`{}`)
|
|
}
|
|
|
|
settings, err := scanDashboardSettings(func(dest ...any) error {
|
|
return s.db.QueryRow(
|
|
r.Context(),
|
|
`
|
|
INSERT INTO dashboard_settings (
|
|
user_id,
|
|
settings
|
|
)
|
|
VALUES (
|
|
$1,
|
|
$2::JSONB
|
|
)
|
|
ON CONFLICT (user_id)
|
|
DO UPDATE SET
|
|
settings = EXCLUDED.settings,
|
|
updated_at = now()
|
|
RETURNING
|
|
user_id::TEXT,
|
|
settings,
|
|
created_at,
|
|
updated_at
|
|
`,
|
|
user.ID,
|
|
string(input.Settings),
|
|
).Scan(dest...)
|
|
})
|
|
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Dashboard-Einstellungen konnten nicht gespeichert werden")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"dashboardSettings": settings,
|
|
})
|
|
}
|
|
|
|
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,
|
|
})
|
|
}
|