1126 lines
28 KiB
Go
1126 lines
28 KiB
Go
// backend\operations.go
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
)
|
|
|
|
type Operation struct {
|
|
ID string `json:"id"`
|
|
OperationNumber string `json:"operationNumber"`
|
|
OperationName string `json:"operationName"`
|
|
Offense string `json:"offense"`
|
|
CaseWorker string `json:"caseWorker"`
|
|
TargetObject string `json:"targetObject"`
|
|
KwAddress string `json:"kwAddress"`
|
|
OperationLeader string `json:"operationLeader"`
|
|
OperationTeam string `json:"operationTeam"`
|
|
Legend string `json:"legend"`
|
|
Camera string `json:"camera"`
|
|
Router string `json:"router"`
|
|
Technology string `json:"technology"`
|
|
Switchbox string `json:"switchbox"`
|
|
HardDrive string `json:"hardDrive"`
|
|
Laptop string `json:"laptop"`
|
|
Remark string `json:"remark"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
type CreateOperationJournalEntryRequest struct {
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
type UpdateOperationJournalEntryRequest struct {
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
type CreateOperationRequest struct {
|
|
OperationNumber string `json:"operationNumber"`
|
|
OperationName string `json:"operationName"`
|
|
Offense string `json:"offense"`
|
|
CaseWorker string `json:"caseWorker"`
|
|
TargetObject string `json:"targetObject"`
|
|
KwAddress string `json:"kwAddress"`
|
|
OperationLeader string `json:"operationLeader"`
|
|
OperationTeam string `json:"operationTeam"`
|
|
Legend string `json:"legend"`
|
|
Camera string `json:"camera"`
|
|
Router string `json:"router"`
|
|
Technology string `json:"technology"`
|
|
Switchbox string `json:"switchbox"`
|
|
HardDrive string `json:"hardDrive"`
|
|
Laptop string `json:"laptop"`
|
|
Remark string `json:"remark"`
|
|
}
|
|
|
|
type UpdateOperationRequest = CreateOperationRequest
|
|
|
|
type OperationHistoryEntry struct {
|
|
ID string `json:"id"`
|
|
OperationID string `json:"operationId"`
|
|
UserID string `json:"userId"`
|
|
UserDisplayName string `json:"userDisplayName"`
|
|
Action string `json:"action"`
|
|
Changes []OperationHistoryChange `json:"changes"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
CanEdit bool `json:"canEdit"`
|
|
}
|
|
|
|
type OperationHistoryChange struct {
|
|
Field string `json:"field"`
|
|
Label string `json:"label"`
|
|
OldValue string `json:"oldValue"`
|
|
NewValue string `json:"newValue"`
|
|
}
|
|
|
|
func (s *Server) handleOperations(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
s.handleListOperations(w, r)
|
|
case http.MethodPost:
|
|
s.handleCreateOperation(w, r)
|
|
default:
|
|
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleOperation(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodPut:
|
|
s.handleUpdateOperation(w, r)
|
|
default:
|
|
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleListOperations(w http.ResponseWriter, r *http.Request) {
|
|
rows, err := s.db.Query(
|
|
r.Context(),
|
|
`
|
|
SELECT
|
|
id,
|
|
operation_number,
|
|
operation_name,
|
|
offense,
|
|
case_worker,
|
|
target_object,
|
|
kw_address,
|
|
operation_leader,
|
|
operation_team,
|
|
legend,
|
|
camera,
|
|
router,
|
|
technology,
|
|
switchbox,
|
|
hard_drive,
|
|
laptop,
|
|
remark,
|
|
created_at,
|
|
updated_at
|
|
FROM operations
|
|
ORDER BY operation_number ASC
|
|
`,
|
|
)
|
|
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Could not load operations")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
operations := []Operation{}
|
|
|
|
for rows.Next() {
|
|
var operation Operation
|
|
|
|
if err := rows.Scan(
|
|
&operation.ID,
|
|
&operation.OperationNumber,
|
|
&operation.OperationName,
|
|
&operation.Offense,
|
|
&operation.CaseWorker,
|
|
&operation.TargetObject,
|
|
&operation.KwAddress,
|
|
&operation.OperationLeader,
|
|
&operation.OperationTeam,
|
|
&operation.Legend,
|
|
&operation.Camera,
|
|
&operation.Router,
|
|
&operation.Technology,
|
|
&operation.Switchbox,
|
|
&operation.HardDrive,
|
|
&operation.Laptop,
|
|
&operation.Remark,
|
|
&operation.CreatedAt,
|
|
&operation.UpdatedAt,
|
|
); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Could not read operations")
|
|
return
|
|
}
|
|
|
|
operations = append(operations, operation)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Could not read operations")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"operations": operations,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleCreateOperation(w http.ResponseWriter, r *http.Request) {
|
|
var input CreateOperationRequest
|
|
|
|
if err := readJSON(r, &input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Invalid JSON")
|
|
return
|
|
}
|
|
|
|
normalizeOperationInput(&input)
|
|
|
|
if input.OperationNumber == "" {
|
|
writeError(w, http.StatusBadRequest, "Einsatznummer ist erforderlich")
|
|
return
|
|
}
|
|
|
|
user, ok := userFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
|
return
|
|
}
|
|
|
|
tx, err := s.db.Begin(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Could not create operation")
|
|
return
|
|
}
|
|
defer tx.Rollback(r.Context())
|
|
|
|
var operation Operation
|
|
|
|
err = tx.QueryRow(
|
|
r.Context(),
|
|
`
|
|
INSERT INTO operations (
|
|
operation_number,
|
|
operation_name,
|
|
offense,
|
|
case_worker,
|
|
target_object,
|
|
kw_address,
|
|
operation_leader,
|
|
operation_team,
|
|
legend,
|
|
camera,
|
|
router,
|
|
technology,
|
|
switchbox,
|
|
hard_drive,
|
|
laptop,
|
|
remark
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
|
RETURNING
|
|
id,
|
|
operation_number,
|
|
operation_name,
|
|
offense,
|
|
case_worker,
|
|
target_object,
|
|
kw_address,
|
|
operation_leader,
|
|
operation_team,
|
|
legend,
|
|
camera,
|
|
router,
|
|
technology,
|
|
switchbox,
|
|
hard_drive,
|
|
laptop,
|
|
remark,
|
|
created_at,
|
|
updated_at
|
|
`,
|
|
input.OperationNumber,
|
|
input.OperationName,
|
|
input.Offense,
|
|
input.CaseWorker,
|
|
input.TargetObject,
|
|
input.KwAddress,
|
|
input.OperationLeader,
|
|
input.OperationTeam,
|
|
input.Legend,
|
|
input.Camera,
|
|
input.Router,
|
|
input.Technology,
|
|
input.Switchbox,
|
|
input.HardDrive,
|
|
input.Laptop,
|
|
input.Remark,
|
|
).Scan(
|
|
&operation.ID,
|
|
&operation.OperationNumber,
|
|
&operation.OperationName,
|
|
&operation.Offense,
|
|
&operation.CaseWorker,
|
|
&operation.TargetObject,
|
|
&operation.KwAddress,
|
|
&operation.OperationLeader,
|
|
&operation.OperationTeam,
|
|
&operation.Legend,
|
|
&operation.Camera,
|
|
&operation.Router,
|
|
&operation.Technology,
|
|
&operation.Switchbox,
|
|
&operation.HardDrive,
|
|
&operation.Laptop,
|
|
&operation.Remark,
|
|
&operation.CreatedAt,
|
|
&operation.UpdatedAt,
|
|
)
|
|
|
|
if err != nil {
|
|
var pgErr *pgconn.PgError
|
|
|
|
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
|
|
writeError(w, http.StatusConflict, "Einsatz mit dieser Einsatznummer existiert bereits")
|
|
return
|
|
}
|
|
|
|
writeError(w, http.StatusInternalServerError, "Could not create operation")
|
|
return
|
|
}
|
|
|
|
changes := buildOperationCreateHistoryChanges(input)
|
|
|
|
if err := recordOperationHistory(r.Context(), tx, operation.ID, user, "created", changes); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Could not create operation history")
|
|
return
|
|
}
|
|
|
|
if err := tx.Commit(r.Context()); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Could not create operation")
|
|
return
|
|
}
|
|
|
|
if err := s.createOperationNotification(
|
|
r.Context(),
|
|
user,
|
|
operation,
|
|
"operation.created",
|
|
"Neuer Einsatz",
|
|
fmt.Sprintf("Einsatz %s wurde erstellt.", operation.OperationNumber),
|
|
map[string]any{
|
|
"operationNumber": operation.OperationNumber,
|
|
"operationName": operation.OperationName,
|
|
},
|
|
); err != nil {
|
|
// nicht abbrechen, Einsatz wurde bereits erstellt
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, map[string]any{
|
|
"operation": operation,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleUpdateOperation(w http.ResponseWriter, r *http.Request) {
|
|
operationID := strings.TrimSpace(r.PathValue("id"))
|
|
|
|
if operationID == "" {
|
|
writeError(w, http.StatusBadRequest, "Einsatz-ID fehlt")
|
|
return
|
|
}
|
|
|
|
var input UpdateOperationRequest
|
|
|
|
if err := readJSON(r, &input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Invalid JSON")
|
|
return
|
|
}
|
|
|
|
normalizeOperationInput(&input)
|
|
|
|
if input.OperationNumber == "" {
|
|
writeError(w, http.StatusBadRequest, "Einsatznummer ist erforderlich")
|
|
return
|
|
}
|
|
|
|
user, ok := userFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
|
return
|
|
}
|
|
|
|
tx, err := s.db.Begin(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Could not update operation")
|
|
return
|
|
}
|
|
defer tx.Rollback(r.Context())
|
|
|
|
oldOperation, err := getOperationForHistory(r.Context(), tx, operationID)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
writeError(w, http.StatusNotFound, "Einsatz wurde nicht gefunden")
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Could not load operation history state")
|
|
return
|
|
}
|
|
|
|
var operation Operation
|
|
|
|
err = tx.QueryRow(
|
|
r.Context(),
|
|
`
|
|
UPDATE operations
|
|
SET
|
|
operation_number = $2,
|
|
operation_name = $3,
|
|
offense = $4,
|
|
case_worker = $5,
|
|
target_object = $6,
|
|
kw_address = $7,
|
|
operation_leader = $8,
|
|
operation_team = $9,
|
|
legend = $10,
|
|
camera = $11,
|
|
router = $12,
|
|
technology = $13,
|
|
switchbox = $14,
|
|
hard_drive = $15,
|
|
laptop = $16,
|
|
remark = $17,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING
|
|
id,
|
|
operation_number,
|
|
operation_name,
|
|
offense,
|
|
case_worker,
|
|
target_object,
|
|
kw_address,
|
|
operation_leader,
|
|
operation_team,
|
|
legend,
|
|
camera,
|
|
router,
|
|
technology,
|
|
switchbox,
|
|
hard_drive,
|
|
laptop,
|
|
remark,
|
|
created_at,
|
|
updated_at
|
|
`,
|
|
operationID,
|
|
input.OperationNumber,
|
|
input.OperationName,
|
|
input.Offense,
|
|
input.CaseWorker,
|
|
input.TargetObject,
|
|
input.KwAddress,
|
|
input.OperationLeader,
|
|
input.OperationTeam,
|
|
input.Legend,
|
|
input.Camera,
|
|
input.Router,
|
|
input.Technology,
|
|
input.Switchbox,
|
|
input.HardDrive,
|
|
input.Laptop,
|
|
input.Remark,
|
|
).Scan(
|
|
&operation.ID,
|
|
&operation.OperationNumber,
|
|
&operation.OperationName,
|
|
&operation.Offense,
|
|
&operation.CaseWorker,
|
|
&operation.TargetObject,
|
|
&operation.KwAddress,
|
|
&operation.OperationLeader,
|
|
&operation.OperationTeam,
|
|
&operation.Legend,
|
|
&operation.Camera,
|
|
&operation.Router,
|
|
&operation.Technology,
|
|
&operation.Switchbox,
|
|
&operation.HardDrive,
|
|
&operation.Laptop,
|
|
&operation.Remark,
|
|
&operation.CreatedAt,
|
|
&operation.UpdatedAt,
|
|
)
|
|
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
writeError(w, http.StatusNotFound, "Einsatz wurde nicht gefunden")
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
var pgErr *pgconn.PgError
|
|
|
|
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
|
|
writeError(w, http.StatusConflict, "Einsatz mit dieser Einsatznummer existiert bereits")
|
|
return
|
|
}
|
|
|
|
writeError(w, http.StatusInternalServerError, "Could not update operation")
|
|
return
|
|
}
|
|
|
|
changes := buildOperationUpdateHistoryChanges(oldOperation, input)
|
|
|
|
if len(changes) > 0 {
|
|
if err := recordOperationHistory(r.Context(), tx, operation.ID, user, "updated", changes); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Could not create operation history")
|
|
return
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit(r.Context()); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Could not update operation")
|
|
return
|
|
}
|
|
|
|
if err := s.createOperationNotification(
|
|
r.Context(),
|
|
user,
|
|
operation,
|
|
"operation.updated",
|
|
"Einsatz geändert",
|
|
fmt.Sprintf("Einsatz %s wurde geändert.", operation.OperationNumber),
|
|
map[string]any{
|
|
"operationNumber": operation.OperationNumber,
|
|
"operationName": operation.OperationName,
|
|
"changes": changes,
|
|
},
|
|
); err != nil {
|
|
// nicht abbrechen, Änderung wurde bereits gespeichert
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"operation": operation,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleListOperationHistory(w http.ResponseWriter, r *http.Request) {
|
|
operationID := strings.TrimSpace(r.PathValue("id"))
|
|
|
|
if operationID == "" {
|
|
writeError(w, http.StatusBadRequest, "Einsatz-ID fehlt")
|
|
return
|
|
}
|
|
|
|
user, ok := userFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
|
return
|
|
}
|
|
|
|
rows, err := s.db.Query(
|
|
r.Context(),
|
|
`
|
|
SELECT
|
|
h.id,
|
|
h.operation_id,
|
|
COALESCE(h.user_id::TEXT, ''),
|
|
COALESCE(NULLIF(u.display_name, ''), NULLIF(u.username, ''), u.email, 'Unbekannt'),
|
|
h.action,
|
|
h.changes,
|
|
h.created_at,
|
|
h.updated_at
|
|
FROM operation_history h
|
|
LEFT JOIN users u ON u.id = h.user_id
|
|
WHERE h.operation_id = $1
|
|
ORDER BY h.created_at DESC
|
|
`,
|
|
operationID,
|
|
)
|
|
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Could not load operation history")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
history := []OperationHistoryEntry{}
|
|
|
|
for rows.Next() {
|
|
var entry OperationHistoryEntry
|
|
var changesRaw []byte
|
|
|
|
if err := rows.Scan(
|
|
&entry.ID,
|
|
&entry.OperationID,
|
|
&entry.UserID,
|
|
&entry.UserDisplayName,
|
|
&entry.Action,
|
|
&changesRaw,
|
|
&entry.CreatedAt,
|
|
&entry.UpdatedAt,
|
|
); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Could not read operation history")
|
|
return
|
|
}
|
|
|
|
if len(changesRaw) > 0 {
|
|
if err := json.Unmarshal(changesRaw, &entry.Changes); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Could not parse operation history")
|
|
return
|
|
}
|
|
}
|
|
|
|
entry.CanEdit =
|
|
entry.Action == "journal" &&
|
|
entry.UserID == user.ID &&
|
|
time.Since(entry.CreatedAt) <= 10*time.Minute
|
|
|
|
history = append(history, entry)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Could not read operation history")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"history": history,
|
|
})
|
|
}
|
|
|
|
func getOperationForHistory(ctx context.Context, tx pgx.Tx, operationID string) (Operation, error) {
|
|
var operation Operation
|
|
|
|
err := tx.QueryRow(
|
|
ctx,
|
|
`
|
|
SELECT
|
|
id,
|
|
operation_number,
|
|
operation_name,
|
|
offense,
|
|
case_worker,
|
|
target_object,
|
|
kw_address,
|
|
operation_leader,
|
|
operation_team,
|
|
legend,
|
|
camera,
|
|
router,
|
|
technology,
|
|
switchbox,
|
|
hard_drive,
|
|
laptop,
|
|
remark,
|
|
created_at,
|
|
updated_at
|
|
FROM operations
|
|
WHERE id = $1
|
|
LIMIT 1
|
|
`,
|
|
operationID,
|
|
).Scan(
|
|
&operation.ID,
|
|
&operation.OperationNumber,
|
|
&operation.OperationName,
|
|
&operation.Offense,
|
|
&operation.CaseWorker,
|
|
&operation.TargetObject,
|
|
&operation.KwAddress,
|
|
&operation.OperationLeader,
|
|
&operation.OperationTeam,
|
|
&operation.Legend,
|
|
&operation.Camera,
|
|
&operation.Router,
|
|
&operation.Technology,
|
|
&operation.Switchbox,
|
|
&operation.HardDrive,
|
|
&operation.Laptop,
|
|
&operation.Remark,
|
|
&operation.CreatedAt,
|
|
&operation.UpdatedAt,
|
|
)
|
|
|
|
return operation, err
|
|
}
|
|
|
|
func recordOperationHistory(
|
|
ctx context.Context,
|
|
tx pgx.Tx,
|
|
operationID string,
|
|
user User,
|
|
action string,
|
|
changes []OperationHistoryChange,
|
|
) error {
|
|
changesJSON, err := json.Marshal(changes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = tx.Exec(
|
|
ctx,
|
|
`
|
|
INSERT INTO operation_history (
|
|
operation_id,
|
|
user_id,
|
|
action,
|
|
changes
|
|
)
|
|
VALUES ($1, $2, $3, $4::JSONB)
|
|
`,
|
|
operationID,
|
|
user.ID,
|
|
action,
|
|
string(changesJSON),
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
func buildOperationCreateHistoryChanges(input CreateOperationRequest) []OperationHistoryChange {
|
|
changes := []OperationHistoryChange{}
|
|
|
|
changes = appendOperationHistoryChange(changes, "operationNumber", "Einsatznummer", "", input.OperationNumber)
|
|
changes = appendOperationHistoryChange(changes, "operationName", "Einsatzname", "", input.OperationName)
|
|
changes = appendOperationHistoryChange(changes, "offense", "Delikt", "", input.Offense)
|
|
changes = appendOperationHistoryChange(changes, "caseWorker", "Sachbearbeitung", "", input.CaseWorker)
|
|
changes = appendOperationHistoryChange(changes, "targetObject", "Zielobjekt", "", input.TargetObject)
|
|
changes = appendOperationHistoryChange(changes, "kwAddress", "KW", "", input.KwAddress)
|
|
changes = appendOperationHistoryChange(changes, "operationLeader", "Einsatzleiter", "", input.OperationLeader)
|
|
changes = appendOperationHistoryChange(changes, "operationTeam", "Einsatzteam", "", input.OperationTeam)
|
|
changes = appendOperationHistoryChange(changes, "legend", "Legende", "", input.Legend)
|
|
changes = appendOperationHistoryChange(changes, "camera", "Kamera", "", input.Camera)
|
|
changes = appendOperationHistoryChange(changes, "router", "Router", "", input.Router)
|
|
changes = appendOperationHistoryChange(changes, "technology", "Technik", "", input.Technology)
|
|
changes = appendOperationHistoryChange(changes, "switchbox", "Switchbox", "", input.Switchbox)
|
|
changes = appendOperationHistoryChange(changes, "hardDrive", "Festplatte", "", input.HardDrive)
|
|
changes = appendOperationHistoryChange(changes, "laptop", "Laptop", "", input.Laptop)
|
|
changes = appendOperationHistoryChange(changes, "remark", "Bemerkung", "", input.Remark)
|
|
|
|
return changes
|
|
}
|
|
|
|
func buildOperationUpdateHistoryChanges(oldOperation Operation, input UpdateOperationRequest) []OperationHistoryChange {
|
|
changes := []OperationHistoryChange{}
|
|
|
|
changes = appendOperationHistoryChange(changes, "operationNumber", "Einsatznummer", oldOperation.OperationNumber, input.OperationNumber)
|
|
changes = appendOperationHistoryChange(changes, "operationName", "Einsatzname", oldOperation.OperationName, input.OperationName)
|
|
changes = appendOperationHistoryChange(changes, "offense", "Delikt", oldOperation.Offense, input.Offense)
|
|
changes = appendOperationHistoryChange(changes, "caseWorker", "Sachbearbeitung", oldOperation.CaseWorker, input.CaseWorker)
|
|
changes = appendOperationHistoryChange(changes, "targetObject", "Zielobjekt", oldOperation.TargetObject, input.TargetObject)
|
|
changes = appendOperationHistoryChange(changes, "kwAddress", "KW", oldOperation.KwAddress, input.KwAddress)
|
|
changes = appendOperationHistoryChange(changes, "operationLeader", "Einsatzleiter", oldOperation.OperationLeader, input.OperationLeader)
|
|
changes = appendOperationHistoryChange(changes, "operationTeam", "Einsatzteam", oldOperation.OperationTeam, input.OperationTeam)
|
|
changes = appendOperationHistoryChange(changes, "legend", "Legende", oldOperation.Legend, input.Legend)
|
|
changes = appendOperationHistoryChange(changes, "camera", "Kamera", oldOperation.Camera, input.Camera)
|
|
changes = appendOperationHistoryChange(changes, "router", "Router", oldOperation.Router, input.Router)
|
|
changes = appendOperationHistoryChange(changes, "technology", "Technik", oldOperation.Technology, input.Technology)
|
|
changes = appendOperationHistoryChange(changes, "switchbox", "Switchbox", oldOperation.Switchbox, input.Switchbox)
|
|
changes = appendOperationHistoryChange(changes, "hardDrive", "Festplatte", oldOperation.HardDrive, input.HardDrive)
|
|
changes = appendOperationHistoryChange(changes, "laptop", "Laptop", oldOperation.Laptop, input.Laptop)
|
|
changes = appendOperationHistoryChange(changes, "remark", "Bemerkung", oldOperation.Remark, input.Remark)
|
|
|
|
return changes
|
|
}
|
|
|
|
func appendOperationHistoryChange(
|
|
changes []OperationHistoryChange,
|
|
field string,
|
|
label string,
|
|
oldValue string,
|
|
newValue string,
|
|
) []OperationHistoryChange {
|
|
oldValue = strings.TrimSpace(oldValue)
|
|
newValue = strings.TrimSpace(newValue)
|
|
|
|
if oldValue == newValue {
|
|
return changes
|
|
}
|
|
|
|
return append(changes, OperationHistoryChange{
|
|
Field: field,
|
|
Label: label,
|
|
OldValue: displayHistoryValue(oldValue),
|
|
NewValue: displayHistoryValue(newValue),
|
|
})
|
|
}
|
|
|
|
func normalizeOperationInput(input *CreateOperationRequest) {
|
|
input.OperationNumber = strings.TrimSpace(input.OperationNumber)
|
|
input.OperationName = strings.TrimSpace(input.OperationName)
|
|
input.Offense = strings.TrimSpace(input.Offense)
|
|
input.CaseWorker = strings.TrimSpace(input.CaseWorker)
|
|
input.TargetObject = strings.TrimSpace(input.TargetObject)
|
|
input.KwAddress = strings.TrimSpace(input.KwAddress)
|
|
input.OperationLeader = strings.TrimSpace(input.OperationLeader)
|
|
input.OperationTeam = strings.TrimSpace(input.OperationTeam)
|
|
input.Legend = strings.TrimSpace(input.Legend)
|
|
input.Camera = strings.TrimSpace(input.Camera)
|
|
input.Router = strings.TrimSpace(input.Router)
|
|
input.Technology = strings.TrimSpace(input.Technology)
|
|
input.Switchbox = strings.TrimSpace(input.Switchbox)
|
|
input.HardDrive = strings.TrimSpace(input.HardDrive)
|
|
input.Laptop = strings.TrimSpace(input.Laptop)
|
|
input.Remark = strings.TrimSpace(input.Remark)
|
|
}
|
|
|
|
func (s *Server) handleCreateOperationJournalEntry(w http.ResponseWriter, r *http.Request) {
|
|
operationID := strings.TrimSpace(r.PathValue("id"))
|
|
|
|
if operationID == "" {
|
|
writeError(w, http.StatusBadRequest, "Einsatz-ID fehlt")
|
|
return
|
|
}
|
|
|
|
var input CreateOperationJournalEntryRequest
|
|
|
|
if err := readJSON(r, &input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Invalid JSON")
|
|
return
|
|
}
|
|
|
|
content := strings.TrimSpace(input.Content)
|
|
|
|
if content == "" {
|
|
writeError(w, http.StatusBadRequest, "Journal-Eintrag darf nicht leer sein")
|
|
return
|
|
}
|
|
|
|
user, ok := userFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
|
return
|
|
}
|
|
|
|
tx, err := s.db.Begin(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Journal-Eintrag konnte nicht erstellt werden")
|
|
return
|
|
}
|
|
defer tx.Rollback(r.Context())
|
|
|
|
operation, err := getOperationForHistory(r.Context(), tx, operationID)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
writeError(w, http.StatusNotFound, "Einsatz wurde nicht gefunden")
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Einsatz konnte nicht geladen werden")
|
|
return
|
|
}
|
|
|
|
changes := []OperationHistoryChange{
|
|
{
|
|
Field: "journal",
|
|
Label: "Journal",
|
|
OldValue: "",
|
|
NewValue: content,
|
|
},
|
|
}
|
|
|
|
if err := recordOperationHistory(r.Context(), tx, operation.ID, user, "journal", changes); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Journal-Eintrag konnte nicht gespeichert werden")
|
|
return
|
|
}
|
|
|
|
if err := tx.Commit(r.Context()); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Journal-Eintrag konnte nicht gespeichert werden")
|
|
return
|
|
}
|
|
|
|
_ = s.createOperationJournalEvent(
|
|
r.Context(),
|
|
user,
|
|
operation,
|
|
map[string]any{
|
|
"operationNumber": operation.OperationNumber,
|
|
"operationName": operation.OperationName,
|
|
},
|
|
)
|
|
|
|
writeJSON(w, http.StatusCreated, map[string]any{
|
|
"ok": true,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleUpdateOperationJournalEntry(w http.ResponseWriter, r *http.Request) {
|
|
operationID := strings.TrimSpace(r.PathValue("id"))
|
|
historyID := strings.TrimSpace(r.PathValue("historyId"))
|
|
|
|
if operationID == "" {
|
|
writeError(w, http.StatusBadRequest, "Einsatz-ID fehlt")
|
|
return
|
|
}
|
|
|
|
if historyID == "" {
|
|
writeError(w, http.StatusBadRequest, "Journal-ID fehlt")
|
|
return
|
|
}
|
|
|
|
var input UpdateOperationJournalEntryRequest
|
|
|
|
if err := readJSON(r, &input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Invalid JSON")
|
|
return
|
|
}
|
|
|
|
content := strings.TrimSpace(input.Content)
|
|
|
|
if content == "" {
|
|
writeError(w, http.StatusBadRequest, "Journal-Eintrag darf nicht leer sein")
|
|
return
|
|
}
|
|
|
|
user, ok := userFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
|
return
|
|
}
|
|
|
|
tx, err := s.db.Begin(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Journal-Eintrag konnte nicht gespeichert werden")
|
|
return
|
|
}
|
|
defer tx.Rollback(r.Context())
|
|
|
|
operation, err := getOperationForHistory(r.Context(), tx, operationID)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
writeError(w, http.StatusNotFound, "Einsatz wurde nicht gefunden")
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Einsatz konnte nicht geladen werden")
|
|
return
|
|
}
|
|
|
|
var changesRaw []byte
|
|
var createdAt time.Time
|
|
|
|
err = tx.QueryRow(
|
|
r.Context(),
|
|
`
|
|
SELECT changes, created_at
|
|
FROM operation_history
|
|
WHERE id = $1
|
|
AND operation_id = $2
|
|
AND user_id = $3
|
|
AND action = 'journal'
|
|
LIMIT 1
|
|
`,
|
|
historyID,
|
|
operationID,
|
|
user.ID,
|
|
).Scan(&changesRaw, &createdAt)
|
|
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
writeError(w, http.StatusNotFound, "Journal-Eintrag wurde nicht gefunden oder darf nicht bearbeitet werden")
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Journal-Eintrag konnte nicht geladen werden")
|
|
return
|
|
}
|
|
|
|
if time.Since(createdAt) > 10*time.Minute {
|
|
writeError(w, http.StatusForbidden, "Journal-Eintrag kann nur innerhalb von 10 Minuten bearbeitet werden")
|
|
return
|
|
}
|
|
|
|
var oldChanges []OperationHistoryChange
|
|
|
|
if len(changesRaw) > 0 {
|
|
if err := json.Unmarshal(changesRaw, &oldChanges); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Journal-Eintrag konnte nicht gelesen werden")
|
|
return
|
|
}
|
|
}
|
|
|
|
oldContent := ""
|
|
|
|
for _, change := range oldChanges {
|
|
if change.Field == "journal" {
|
|
oldContent = change.NewValue
|
|
break
|
|
}
|
|
}
|
|
|
|
if strings.TrimSpace(oldContent) == content {
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"ok": true,
|
|
})
|
|
return
|
|
}
|
|
|
|
changes := []OperationHistoryChange{
|
|
{
|
|
Field: "journal",
|
|
Label: "Journal",
|
|
OldValue: oldContent,
|
|
NewValue: content,
|
|
},
|
|
}
|
|
|
|
changesJSON, err := json.Marshal(changes)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Journal-Eintrag konnte nicht vorbereitet werden")
|
|
return
|
|
}
|
|
|
|
commandTag, err := tx.Exec(
|
|
r.Context(),
|
|
`
|
|
UPDATE operation_history
|
|
SET
|
|
changes = $4::JSONB,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
AND operation_id = $2
|
|
AND user_id = $3
|
|
AND action = 'journal'
|
|
AND created_at >= now() - interval '10 minutes'
|
|
`,
|
|
historyID,
|
|
operationID,
|
|
user.ID,
|
|
string(changesJSON),
|
|
)
|
|
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Journal-Eintrag konnte nicht gespeichert werden")
|
|
return
|
|
}
|
|
|
|
if commandTag.RowsAffected() == 0 {
|
|
writeError(w, http.StatusForbidden, "Journal-Eintrag kann nicht mehr bearbeitet werden")
|
|
return
|
|
}
|
|
|
|
if err := tx.Commit(r.Context()); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Journal-Eintrag konnte nicht gespeichert werden")
|
|
return
|
|
}
|
|
|
|
_ = s.createOperationJournalEvent(
|
|
r.Context(),
|
|
user,
|
|
operation,
|
|
map[string]any{
|
|
"operationNumber": operation.OperationNumber,
|
|
"operationName": operation.OperationName,
|
|
"historyId": historyID,
|
|
"action": "updated",
|
|
},
|
|
)
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"ok": true,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleListUnreadOperationJournals(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 entity_id::TEXT
|
|
FROM notifications
|
|
WHERE user_id = $1
|
|
AND visible = false
|
|
AND type = 'operation.journal'
|
|
AND entity_type = 'operation'
|
|
AND entity_id IS NOT NULL
|
|
AND read_at IS NULL
|
|
GROUP BY entity_id
|
|
`,
|
|
user.ID,
|
|
)
|
|
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Ungelesene Journal-Einträge konnten nicht geladen werden")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
operationIds := []string{}
|
|
|
|
for rows.Next() {
|
|
var operationID string
|
|
if err := rows.Scan(&operationID); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Ungelesene Journal-Einträge konnten nicht gelesen werden")
|
|
return
|
|
}
|
|
|
|
operationIds = append(operationIds, operationID)
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"operationIds": operationIds,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleMarkOperationJournalRead(w http.ResponseWriter, r *http.Request) {
|
|
user, ok := userFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
|
return
|
|
}
|
|
|
|
operationID := strings.TrimSpace(r.PathValue("id"))
|
|
if operationID == "" {
|
|
writeError(w, http.StatusBadRequest, "Einsatz-ID fehlt")
|
|
return
|
|
}
|
|
|
|
_, err := s.db.Exec(
|
|
r.Context(),
|
|
`
|
|
UPDATE notifications
|
|
SET read_at = COALESCE(read_at, now())
|
|
WHERE user_id = $1
|
|
AND visible = false
|
|
AND type = 'operation.journal'
|
|
AND entity_type = 'operation'
|
|
AND entity_id = $2
|
|
AND read_at IS NULL
|
|
`,
|
|
user.ID,
|
|
operationID,
|
|
)
|
|
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Journal-Status konnte nicht aktualisiert werden")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"ok": true,
|
|
})
|
|
}
|