// backend\device_lookups.go package main import ( "context" "fmt" "net/http" "strings" "github.com/jackc/pgx/v5" ) type DeviceLookupOption struct { ID string `json:"id"` Name string `json:"name"` } type CreateDeviceLookupRequest struct { Name string `json:"name"` } func (s *Server) handleDeviceLookups(w http.ResponseWriter, r *http.Request) { manufacturers, err := listDeviceLookupOptions(r.Context(), s.db, "device_manufacturers") if err != nil { writeError(w, http.StatusInternalServerError, "Hersteller konnten nicht geladen werden") return } locations, err := listDeviceLookupOptions(r.Context(), s.db, "device_locations") if err != nil { writeError(w, http.StatusInternalServerError, "Standorte konnten nicht geladen werden") return } writeJSON(w, http.StatusOK, map[string]any{ "manufacturers": manufacturers, "locations": locations, }) } func (s *Server) handleCreateDeviceManufacturer(w http.ResponseWriter, r *http.Request) { s.handleCreateDeviceLookup(w, r, "device_manufacturers", "Hersteller") } func (s *Server) handleDeleteDeviceManufacturer(w http.ResponseWriter, r *http.Request) { s.handleDeleteDeviceLookup(w, r, "device_manufacturers", "Hersteller") } func (s *Server) handleCreateDeviceLocation(w http.ResponseWriter, r *http.Request) { s.handleCreateDeviceLookup(w, r, "device_locations", "Standort") } func (s *Server) handleCreateDeviceLookup( w http.ResponseWriter, r *http.Request, tableName string, label string, ) { var input CreateDeviceLookupRequest if err := readJSON(r, &input); err != nil { writeError(w, http.StatusBadRequest, "Invalid JSON") return } name := strings.TrimSpace(input.Name) if name == "" { writeError(w, http.StatusBadRequest, label+" ist erforderlich") return } option, err := upsertDeviceLookupOption(r.Context(), s.db, tableName, name) if err != nil { writeError(w, http.StatusInternalServerError, label+" konnte nicht gespeichert werden") return } writeJSON(w, http.StatusCreated, map[string]any{ "option": option, }) } func (s *Server) handleDeleteDeviceLookup( w http.ResponseWriter, r *http.Request, tableName string, label string, ) { id := strings.TrimSpace(r.PathValue("id")) if id == "" { writeError(w, http.StatusBadRequest, label+" ist erforderlich") return } commandTag, err := s.db.Exec( r.Context(), fmt.Sprintf(`DELETE FROM %s WHERE id = $1`, tableName), id, ) if err != nil { writeError(w, http.StatusInternalServerError, label+" konnte nicht gelöscht werden") return } if commandTag.RowsAffected() == 0 { writeError(w, http.StatusNotFound, label+" wurde nicht gefunden") return } writeJSON(w, http.StatusOK, map[string]any{ "deleted": true, }) } func listDeviceLookupOptions( ctx context.Context, db interface { Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) }, tableName string, ) ([]DeviceLookupOption, error) { query := fmt.Sprintf( ` SELECT id::TEXT, name FROM %s ORDER BY lower(name) ASC `, tableName, ) rows, err := db.Query(ctx, query) if err != nil { return nil, err } defer rows.Close() options := []DeviceLookupOption{} for rows.Next() { var option DeviceLookupOption if err := rows.Scan(&option.ID, &option.Name); err != nil { return nil, err } options = append(options, option) } if err := rows.Err(); err != nil { return nil, err } return options, nil } func ensureDeviceLookupValues( ctx context.Context, tx pgx.Tx, manufacturer string, location string, ) error { if err := ensureOptionalDeviceLookupValue(ctx, tx, "device_manufacturers", manufacturer); err != nil { return err } if err := ensureOptionalDeviceLookupValue(ctx, tx, "device_locations", location); err != nil { return err } return nil } func ensureOptionalDeviceLookupValue( ctx context.Context, db deviceTransaction, tableName string, name string, ) error { name = strings.TrimSpace(name) if name == "" { return nil } _, err := upsertDeviceLookupOption(ctx, db, tableName, name) return err } func upsertDeviceLookupOption( ctx context.Context, db deviceTransaction, tableName string, name string, ) (DeviceLookupOption, error) { name = strings.TrimSpace(name) normalizedName := strings.ToLower(name) query := fmt.Sprintf( ` INSERT INTO %s ( name, normalized_name ) VALUES ($1, $2) ON CONFLICT (normalized_name) DO UPDATE SET updated_at = now() RETURNING id::TEXT, name `, tableName, ) var option DeviceLookupOption err := db.QueryRow( ctx, query, name, normalizedName, ).Scan(&option.ID, &option.Name) return option, err }