402 lines
7.3 KiB
Go
402 lines
7.3 KiB
Go
// backend\search.go
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type globalSearchResponse struct {
|
|
Query string `json:"query"`
|
|
Groups []globalSearchGroup `json:"groups"`
|
|
}
|
|
|
|
type globalSearchGroup struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Items []globalSearchItem `json:"items"`
|
|
}
|
|
|
|
type globalSearchItem struct {
|
|
ID string `json:"id"`
|
|
EntityID string `json:"entityId"`
|
|
EntityType string `json:"entityType"`
|
|
Title string `json:"title"`
|
|
Subtitle string `json:"subtitle"`
|
|
Href string `json:"href"`
|
|
Data map[string]any `json:"data,omitempty"`
|
|
}
|
|
|
|
func (s *Server) handleGlobalSearch(w http.ResponseWriter, r *http.Request) {
|
|
user, ok := userFromContext(r.Context())
|
|
if !ok {
|
|
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
|
return
|
|
}
|
|
|
|
query := strings.TrimSpace(r.URL.Query().Get("q"))
|
|
limit := parseSearchLimit(r.URL.Query().Get("limit"), 8)
|
|
|
|
if len([]rune(query)) < 2 {
|
|
writeJSON(w, http.StatusOK, globalSearchResponse{
|
|
Query: query,
|
|
Groups: []globalSearchGroup{},
|
|
})
|
|
return
|
|
}
|
|
|
|
groups := make([]globalSearchGroup, 0, 3)
|
|
|
|
if userCanSearch(user, "operations:read") {
|
|
items, err := s.searchOperations(r.Context(), query, limit)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Einsätze konnten nicht durchsucht werden")
|
|
return
|
|
}
|
|
|
|
if len(items) > 0 {
|
|
groups = append(groups, globalSearchGroup{
|
|
ID: "operations",
|
|
Title: "Einsätze",
|
|
Items: items,
|
|
})
|
|
}
|
|
}
|
|
|
|
if userCanSearch(user, "devices:read") {
|
|
items, err := s.searchDevices(r.Context(), query, limit)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Geräte konnten nicht durchsucht werden")
|
|
return
|
|
}
|
|
|
|
if len(items) > 0 {
|
|
groups = append(groups, globalSearchGroup{
|
|
ID: "devices",
|
|
Title: "Geräte",
|
|
Items: items,
|
|
})
|
|
}
|
|
}
|
|
|
|
if userCanSearch(user, "users:read") {
|
|
items, err := s.searchUsers(r.Context(), query, limit)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Benutzer konnten nicht durchsucht werden")
|
|
return
|
|
}
|
|
|
|
if len(items) > 0 {
|
|
groups = append(groups, globalSearchGroup{
|
|
ID: "users",
|
|
Title: "Benutzer",
|
|
Items: items,
|
|
})
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, globalSearchResponse{
|
|
Query: query,
|
|
Groups: groups,
|
|
})
|
|
}
|
|
|
|
func (s *Server) searchOperations(ctx context.Context, query string, limit int) ([]globalSearchItem, error) {
|
|
pattern := "%" + query + "%"
|
|
|
|
rows, err := s.db.Query(
|
|
ctx,
|
|
`
|
|
SELECT
|
|
id::TEXT,
|
|
operation_number,
|
|
operation_name,
|
|
offense,
|
|
kw_address,
|
|
operation_leader,
|
|
operation_team
|
|
FROM operations
|
|
WHERE CONCAT_WS(
|
|
' ',
|
|
operation_number,
|
|
operation_name,
|
|
offense,
|
|
case_worker,
|
|
target_object,
|
|
kw_address,
|
|
operation_leader,
|
|
operation_team,
|
|
legend,
|
|
remark
|
|
) ILIKE $1
|
|
ORDER BY updated_at DESC
|
|
LIMIT $2
|
|
`,
|
|
pattern,
|
|
limit,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := []globalSearchItem{}
|
|
|
|
for rows.Next() {
|
|
var id string
|
|
var operationNumber string
|
|
var operationName string
|
|
var offense string
|
|
var kwAddress string
|
|
var operationLeader string
|
|
var operationTeam string
|
|
|
|
if err := rows.Scan(
|
|
&id,
|
|
&operationNumber,
|
|
&operationName,
|
|
&offense,
|
|
&kwAddress,
|
|
&operationLeader,
|
|
&operationTeam,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
title := operationName
|
|
if strings.TrimSpace(title) == "" {
|
|
title = operationNumber
|
|
}
|
|
|
|
subtitle := joinSearchSubtitle(
|
|
operationNumber,
|
|
offense,
|
|
kwAddress,
|
|
operationLeader,
|
|
operationTeam,
|
|
)
|
|
|
|
items = append(items, globalSearchItem{
|
|
ID: "operation-" + id,
|
|
EntityID: id,
|
|
EntityType: "operation",
|
|
Title: title,
|
|
Subtitle: subtitle,
|
|
Href: "/einsaetze",
|
|
Data: map[string]any{
|
|
"operationNumber": operationNumber,
|
|
},
|
|
})
|
|
}
|
|
|
|
return items, rows.Err()
|
|
}
|
|
|
|
func (s *Server) searchDevices(ctx context.Context, query string, limit int) ([]globalSearchItem, error) {
|
|
pattern := "%" + query + "%"
|
|
|
|
rows, err := s.db.Query(
|
|
ctx,
|
|
`
|
|
SELECT
|
|
id::TEXT,
|
|
inventory_number,
|
|
manufacturer,
|
|
model,
|
|
serial_number,
|
|
mac_address,
|
|
location,
|
|
loan_status
|
|
FROM devices
|
|
WHERE CONCAT_WS(
|
|
' ',
|
|
inventory_number,
|
|
manufacturer,
|
|
model,
|
|
serial_number,
|
|
mac_address,
|
|
location,
|
|
loan_status,
|
|
comment
|
|
) ILIKE $1
|
|
ORDER BY updated_at DESC
|
|
LIMIT $2
|
|
`,
|
|
pattern,
|
|
limit,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := []globalSearchItem{}
|
|
|
|
for rows.Next() {
|
|
var id string
|
|
var inventoryNumber string
|
|
var manufacturer string
|
|
var model string
|
|
var serialNumber string
|
|
var macAddress string
|
|
var location string
|
|
var loanStatus string
|
|
|
|
if err := rows.Scan(
|
|
&id,
|
|
&inventoryNumber,
|
|
&manufacturer,
|
|
&model,
|
|
&serialNumber,
|
|
&macAddress,
|
|
&location,
|
|
&loanStatus,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
items = append(items, globalSearchItem{
|
|
ID: "device-" + id,
|
|
EntityID: id,
|
|
EntityType: "device",
|
|
Title: inventoryNumber,
|
|
Subtitle: joinSearchSubtitle(
|
|
manufacturer,
|
|
model,
|
|
serialNumber,
|
|
macAddress,
|
|
location,
|
|
loanStatus,
|
|
),
|
|
Href: "/geraete",
|
|
Data: map[string]any{
|
|
"inventoryNumber": inventoryNumber,
|
|
},
|
|
})
|
|
}
|
|
|
|
return items, rows.Err()
|
|
}
|
|
|
|
func (s *Server) searchUsers(ctx context.Context, query string, limit int) ([]globalSearchItem, error) {
|
|
pattern := "%" + query + "%"
|
|
|
|
rows, err := s.db.Query(
|
|
ctx,
|
|
`
|
|
SELECT
|
|
id::TEXT,
|
|
COALESCE(username, ''),
|
|
COALESCE(display_name, ''),
|
|
email,
|
|
COALESCE(unit, ''),
|
|
COALESCE(user_group, '')
|
|
FROM users
|
|
WHERE CONCAT_WS(
|
|
' ',
|
|
username,
|
|
display_name,
|
|
email,
|
|
unit,
|
|
user_group
|
|
) ILIKE $1
|
|
ORDER BY display_name ASC, username ASC, email ASC
|
|
LIMIT $2
|
|
`,
|
|
pattern,
|
|
limit,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := []globalSearchItem{}
|
|
|
|
for rows.Next() {
|
|
var id string
|
|
var username string
|
|
var displayName string
|
|
var email string
|
|
var unit string
|
|
var group string
|
|
|
|
if err := rows.Scan(
|
|
&id,
|
|
&username,
|
|
&displayName,
|
|
&email,
|
|
&unit,
|
|
&group,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
title := displayName
|
|
if strings.TrimSpace(title) == "" {
|
|
title = username
|
|
}
|
|
if strings.TrimSpace(title) == "" {
|
|
title = email
|
|
}
|
|
|
|
items = append(items, globalSearchItem{
|
|
ID: "user-" + id,
|
|
EntityID: id,
|
|
EntityType: "user",
|
|
Title: title,
|
|
Subtitle: joinSearchSubtitle(email, unit, group),
|
|
Href: "/administration",
|
|
Data: map[string]any{
|
|
"username": username,
|
|
"email": email,
|
|
},
|
|
})
|
|
}
|
|
|
|
return items, rows.Err()
|
|
}
|
|
|
|
func parseSearchLimit(value string, fallback int) int {
|
|
limit, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
|
|
if limit < 1 {
|
|
return fallback
|
|
}
|
|
|
|
if limit > 25 {
|
|
return 25
|
|
}
|
|
|
|
return limit
|
|
}
|
|
|
|
func userCanSearch(user User, right string) bool {
|
|
for _, userRight := range user.Rights {
|
|
if userRight == "admin" || userRight == right {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func joinSearchSubtitle(values ...string) string {
|
|
parts := make([]string, 0, len(values))
|
|
|
|
for _, value := range values {
|
|
value = strings.TrimSpace(value)
|
|
if value != "" {
|
|
parts = append(parts, value)
|
|
}
|
|
}
|
|
|
|
return strings.Join(parts, " · ")
|
|
}
|