// 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, }) }