From 47bf652b56f83052ce636144139678f4127db9e4 Mon Sep 17 00:00:00 2001 From: Linrador <68631622+Linrador@users.noreply.github.com> Date: Thu, 28 May 2026 15:06:37 +0200 Subject: [PATCH] updated --- backend/.env | 2 +- backend/address_reverse.go | 192 +++ backend/address_search.go | 17 +- backend/admin.go | 44 +- backend/admin_milestone.go | 24 +- backend/camera_firmware.go | 2 +- backend/cors.go | 2 +- backend/device_events.go | 57 +- backend/devices.go | 52 +- backend/feedback.go | 63 +- backend/history.go | 226 ++- backend/milestone_api.go | 614 ++++++++ backend/milestone_devices.go | 39 +- backend/milestone_hardware.go | 338 ---- backend/notification_preferences.go | 61 +- backend/notifications.go | 470 +++--- backend/operation_assignees.go | 101 ++ backend/operations.go | 17 +- backend/routes.go | 17 +- backend/setup/main.go | 23 +- backend/user_events.go | 33 + backend/user_settings.go | 2 + backend/user_unit_lookups.go | 21 + frontend/package-lock.json | 1 + frontend/package.json | 1 + .../public/sounds/notifications/alert.mp3 | Bin 0 -> 25389 bytes .../public/sounds/notifications/chime.mp3 | Bin 0 -> 49581 bytes .../public/sounds/notifications/default.mp3 | Bin 0 -> 25389 bytes frontend/public/sounds/notifications/soft.mp3 | Bin 0 -> 25389 bytes .../public/sounds/notifications/success.mp3 | Bin 0 -> 25389 bytes frontend/src/App.tsx | 271 ++-- frontend/src/components/AddressCombobox.tsx | 344 ++++- frontend/src/components/AddressMapPicker.tsx | 332 ++++ frontend/src/components/AppLayout.tsx | 6 +- frontend/src/components/Badge.tsx | 35 + frontend/src/components/ButtonGroup.tsx | 58 +- frontend/src/components/Camera.tsx | 158 +- frontend/src/components/Combobox.tsx | 35 +- frontend/src/components/Map.tsx | 129 +- frontend/src/components/Modal.tsx | 28 +- frontend/src/components/MultiCombobox.tsx | 201 +++ .../src/components/NotificationCenter.tsx | 16 +- frontend/src/components/Notifications.tsx | 149 +- frontend/src/components/RadioGroup.tsx | 369 +++++ frontend/src/components/Sidebar.tsx | 4 +- frontend/src/components/TaskList.tsx | 16 +- frontend/src/components/Topbar.tsx | 15 +- frontend/src/components/types.ts | 10 +- .../administration/milestone/Milestone.tsx | 72 +- .../teams/TeamsAdministration.tsx | 236 ++- .../users/UsersAdministration.tsx | 1353 ++++++++++++----- .../src/pages/devices/CreateDeviceModal.tsx | 31 +- .../src/pages/devices/DeviceFormFields.tsx | 172 ++- .../src/pages/devices/DeviceModalTabs.tsx | 22 +- frontend/src/pages/devices/DevicesPage.tsx | 286 +++- .../src/pages/devices/EditDeviceModal.tsx | 96 +- .../pages/operations/CreateOperationModal.tsx | 845 ++++++++-- .../src/pages/operations/OperationMap.tsx | 19 +- .../src/pages/operations/OperationModal.tsx | 385 ++++- .../src/pages/operations/OperationsPage.tsx | 1039 +++++++++++-- frontend/src/pages/settings/SettingsPage.tsx | 20 - .../benachrichtigungen/Benachrichtigungen.tsx | 157 +- frontend/src/pages/settings/konto/Konto.tsx | 154 +- 63 files changed, 7312 insertions(+), 2170 deletions(-) create mode 100644 backend/address_reverse.go create mode 100644 backend/milestone_api.go delete mode 100644 backend/milestone_hardware.go create mode 100644 backend/operation_assignees.go create mode 100644 backend/user_events.go create mode 100644 backend/user_unit_lookups.go create mode 100644 frontend/public/sounds/notifications/alert.mp3 create mode 100644 frontend/public/sounds/notifications/chime.mp3 create mode 100644 frontend/public/sounds/notifications/default.mp3 create mode 100644 frontend/public/sounds/notifications/soft.mp3 create mode 100644 frontend/public/sounds/notifications/success.mp3 create mode 100644 frontend/src/components/AddressMapPicker.tsx create mode 100644 frontend/src/components/MultiCombobox.tsx create mode 100644 frontend/src/components/RadioGroup.tsx diff --git a/backend/.env b/backend/.env index 8b9a39d..8fca91b 100644 --- a/backend/.env +++ b/backend/.env @@ -17,4 +17,4 @@ ADMIN_GROUP=admin ADMIN_RIGHTS=admin,users:read,users:write ADMIN_TEAM_NAME=Administration ADMIN_TEAM_INITIAL=A -ADMIN_TEAM_HREF=# \ No newline at end of file +ADMIN_TEAM_HREF=#administration \ No newline at end of file diff --git a/backend/address_reverse.go b/backend/address_reverse.go new file mode 100644 index 0000000..4190767 --- /dev/null +++ b/backend/address_reverse.go @@ -0,0 +1,192 @@ +// backend\address_reverse.go + +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "time" +) + +type reverseAddressSuggestion struct { + ID string `json:"id"` + Label string `json:"label"` + Lat float64 `json:"lat"` + Lon float64 `json:"lon"` + Type string `json:"type,omitempty"` + GeoJSON json.RawMessage `json:"geojson,omitempty"` +} + +type nominatimReverseAddress struct { + HouseNumber string `json:"house_number"` + Road string `json:"road"` + Footway string `json:"footway"` + Pedestrian string `json:"pedestrian"` + Path string `json:"path"` + Suburb string `json:"suburb"` + Quarter string `json:"quarter"` + CityDistrict string `json:"city_district"` + Borough string `json:"borough"` + City string `json:"city"` + Town string `json:"town"` + Village string `json:"village"` + Municipality string `json:"municipality"` + County string `json:"county"` + State string `json:"state"` + Postcode string `json:"postcode"` + Country string `json:"country"` +} + +type nominatimReverseResponse struct { + PlaceID int64 `json:"place_id"` + OSMType string `json:"osm_type"` + OSMID int64 `json:"osm_id"` + DisplayName string `json:"display_name"` + Lat string `json:"lat"` + Lon string `json:"lon"` + Type string `json:"type"` + Address nominatimReverseAddress `json:"address"` + GeoJSON json.RawMessage `json:"geojson,omitempty"` +} + +func appendUnique(parts []string, value string) []string { + value = strings.TrimSpace(value) + + if value == "" { + return parts + } + + for _, part := range parts { + if strings.EqualFold(part, value) { + return parts + } + } + + return append(parts, value) +} + +func formatReverseAddressLabel(result nominatimReverseResponse) string { + address := result.Address + + street := firstNonEmpty( + address.Road, + address.Footway, + address.Pedestrian, + address.Path, + ) + + streetLine := street + + if street != "" && strings.TrimSpace(address.HouseNumber) != "" { + streetLine = fmt.Sprintf("%s %s", street, strings.TrimSpace(address.HouseNumber)) + } + + city := firstNonEmpty( + address.City, + address.Town, + address.Village, + address.Municipality, + ) + + postcodeCity := city + + if strings.TrimSpace(address.Postcode) != "" && city != "" { + postcodeCity = fmt.Sprintf("%s %s", strings.TrimSpace(address.Postcode), city) + } + + parts := []string{} + + parts = appendUnique(parts, streetLine) + parts = appendUnique(parts, postcodeCity) + parts = appendUnique(parts, address.State) + parts = appendUnique(parts, address.Country) + + if len(parts) > 0 { + return strings.Join(parts, ", ") + } + + return strings.TrimSpace(result.DisplayName) +} + +func (s *Server) handleAddressReverse(w http.ResponseWriter, r *http.Request) { + latRaw := strings.TrimSpace(r.URL.Query().Get("lat")) + lonRaw := strings.TrimSpace(r.URL.Query().Get("lon")) + + lat, err := strconv.ParseFloat(latRaw, 64) + if err != nil || lat < -90 || lat > 90 { + writeError(w, http.StatusBadRequest, "Ungültige Latitude") + return + } + + lon, err := strconv.ParseFloat(lonRaw, 64) + if err != nil || lon < -180 || lon > 180 { + writeError(w, http.StatusBadRequest, "Ungültige Longitude") + return + } + + params := url.Values{} + params.Set("format", "jsonv2") + params.Set("addressdetails", "1") + params.Set("polygon_geojson", "1") + params.Set("lat", strconv.FormatFloat(lat, 'f', 7, 64)) + params.Set("lon", strconv.FormatFloat(lon, 'f', 7, 64)) + params.Set("zoom", "18") + params.Set("accept-language", "de") + + reverseURL := fmt.Sprintf( + "https://nominatim.openstreetmap.org/reverse?%s", + params.Encode(), + ) + + request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, reverseURL, nil) + if err != nil { + writeError(w, http.StatusInternalServerError, "Adresse konnte nicht vorbereitet werden") + return + } + + request.Header.Set("User-Agent", "TEG/1.0 address reverse lookup") + + client := &http.Client{ + Timeout: 8 * time.Second, + } + + response, err := client.Do(request) + if err != nil { + writeError(w, http.StatusBadGateway, "Adresse konnte nicht ermittelt werden") + return + } + defer response.Body.Close() + + if response.StatusCode < 200 || response.StatusCode >= 300 { + writeError(w, http.StatusBadGateway, "Adresse konnte nicht ermittelt werden") + return + } + + var result nominatimReverseResponse + + if err := json.NewDecoder(response.Body).Decode(&result); err != nil { + writeError(w, http.StatusBadGateway, "Adresse konnte nicht gelesen werden") + return + } + + label := formatReverseAddressLabel(result) + if label == "" { + writeError(w, http.StatusNotFound, "Für diese Position wurde keine Adresse gefunden") + return + } + + writeJSON(w, http.StatusOK, map[string]any{ + "suggestion": reverseAddressSuggestion{ + ID: fmt.Sprintf("reverse-%d-%d", result.PlaceID, time.Now().UnixNano()), + Label: label, + Lat: lat, + Lon: lon, + Type: result.Type, + GeoJSON: result.GeoJSON, + }, + }) +} diff --git a/backend/address_search.go b/backend/address_search.go index e295988..e4a60b8 100644 --- a/backend/address_search.go +++ b/backend/address_search.go @@ -49,11 +49,12 @@ type photonGeometry struct { } type nominatimSearchResult struct { - PlaceID int64 `json:"place_id"` - DisplayName string `json:"display_name"` - Lat string `json:"lat"` - Lon string `json:"lon"` - Type string `json:"type"` + PlaceID int64 `json:"place_id"` + DisplayName string `json:"display_name"` + Lat string `json:"lat"` + Lon string `json:"lon"` + Type string `json:"type"` + Address nominatimReverseAddress `json:"address"` } func (s *Server) handleAddressSearch(w http.ResponseWriter, r *http.Request) { @@ -234,7 +235,11 @@ func (s *Server) handleNominatimAddressSearch( suggestions := make([]AddressSuggestion, 0, len(results)) for _, result := range results { - label := strings.TrimSpace(result.DisplayName) + label := formatReverseAddressLabel(nominatimReverseResponse{ + DisplayName: result.DisplayName, + Address: result.Address, + }) + if label == "" { continue } diff --git a/backend/admin.go b/backend/admin.go index d5b02a8..24010d0 100644 --- a/backend/admin.go +++ b/backend/admin.go @@ -72,6 +72,10 @@ func normalizeAdminUserInput(input *AdminUserRequest) { input.TeamIDs = cleanStringList(input.TeamIDs) } +func ensureAdminUserLookupValues(ctx context.Context, tx pgx.Tx, unit string) error { + return ensureOptionalDeviceLookupValue(ctx, tx, "user_units", unit) +} + func normalizeAdminTeamInput(input *AdminTeamRequest) { input.Name = strings.TrimSpace(input.Name) input.Initial = strings.TrimSpace(input.Initial) @@ -130,7 +134,7 @@ func (s *Server) handleAdminListUsers(w http.ResponseWriter, r *http.Request) { defer rows.Close() users := []AdminUser{} - userByID := map[string]*AdminUser{} + userIndexByID := map[string]int{} for rows.Next() { var user AdminUser @@ -152,8 +156,14 @@ func (s *Server) handleAdminListUsers(w http.ResponseWriter, r *http.Request) { } user.Teams = []AdminTeam{} + users = append(users, user) - userByID[user.ID] = &users[len(users)-1] + userIndexByID[user.ID] = len(users) - 1 + } + + if err := rows.Err(); err != nil { + writeError(w, http.StatusInternalServerError, "Benutzer konnten nicht vollständig gelesen werden") + return } teamRows, err := s.db.Query( @@ -196,9 +206,17 @@ func (s *Server) handleAdminListUsers(w http.ResponseWriter, r *http.Request) { return } - if user := userByID[userID]; user != nil { - user.Teams = append(user.Teams, team) + userIndex, ok := userIndexByID[userID] + if !ok { + continue } + + users[userIndex].Teams = append(users[userIndex].Teams, team) + } + + if err := teamRows.Err(); err != nil { + writeError(w, http.StatusInternalServerError, "Team-Zuordnungen konnten nicht vollständig gelesen werden") + return } writeJSON(w, http.StatusOK, map[string]any{ @@ -239,6 +257,11 @@ func (s *Server) handleAdminCreateUser(w http.ResponseWriter, r *http.Request) { } defer tx.Rollback(r.Context()) + if err := ensureAdminUserLookupValues(r.Context(), tx, input.Unit); err != nil { + writeError(w, http.StatusInternalServerError, "Einheit konnte nicht gespeichert werden") + return + } + var userID string err = tx.QueryRow( @@ -289,6 +312,10 @@ func (s *Server) handleAdminCreateUser(w http.ResponseWriter, r *http.Request) { return } + if createdUser, err := s.getUserByID(r.Context(), userID); err == nil { + s.publishUserProfileEvent("user.created", createdUser) + } + writeJSON(w, http.StatusCreated, map[string]any{ "ok": true, }) @@ -323,6 +350,11 @@ func (s *Server) handleAdminUpdateUser(w http.ResponseWriter, r *http.Request) { } defer tx.Rollback(r.Context()) + if err := ensureAdminUserLookupValues(r.Context(), tx, input.Unit); err != nil { + writeError(w, http.StatusInternalServerError, "Einheit konnte nicht gespeichert werden") + return + } + if input.Password != "" { passwordHash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) if err != nil { @@ -403,6 +435,10 @@ func (s *Server) handleAdminUpdateUser(w http.ResponseWriter, r *http.Request) { return } + if updatedUser, err := s.getUserByID(r.Context(), userID); err == nil { + s.publishUserProfileEvent("user.updated", updatedUser) + } + writeJSON(w, http.StatusOK, map[string]any{ "ok": true, }) diff --git a/backend/admin_milestone.go b/backend/admin_milestone.go index 9e14bd1..8d3f22c 100644 --- a/backend/admin_milestone.go +++ b/backend/admin_milestone.go @@ -149,6 +149,7 @@ type milestoneHardwareDevice struct { Manufacturer string MacAddress string IPAddress string + Port string DeviceModelName string SerialNumber string FirmwareVersion string @@ -830,7 +831,7 @@ func (s *Server) requestMilestoneHardwareDevices( enabled = !*hardware.Disabled } - ipAddress := extractMilestoneHardwareIPAddress(hardware.Address) + ipAddress, port := extractMilestoneHardwareAddressParts(hardware.Address) result = append(result, milestoneHardwareDevice{ HardwareID: hardwareID, @@ -840,6 +841,7 @@ func (s *Server) requestMilestoneHardwareDevices( Manufacturer: manufacturer, MacAddress: strings.TrimSpace(settings.MacAddress), IPAddress: ipAddress, + Port: port, DeviceModelName: model, SerialNumber: strings.TrimSpace(settings.SerialNumber), FirmwareVersion: strings.TrimSpace(settings.FirmwareVersion), @@ -857,25 +859,35 @@ func (s *Server) requestMilestoneHardwareDevices( } func extractMilestoneHardwareIPAddress(value string) string { + host, _ := extractMilestoneHardwareAddressParts(value) + return host +} + +func extractMilestoneHardwarePort(value string) string { + _, port := extractMilestoneHardwareAddressParts(value) + return port +} + +func extractMilestoneHardwareAddressParts(value string) (string, string) { value = strings.TrimSpace(value) if value == "" { - return "" + return "", "" } parsedURL, err := url.Parse(value) if err == nil && parsedURL.Hostname() != "" { - return parsedURL.Hostname() + return strings.TrimSpace(parsedURL.Hostname()), strings.TrimSpace(parsedURL.Port()) } value = strings.TrimPrefix(value, "http://") value = strings.TrimPrefix(value, "https://") value = strings.TrimRight(value, "/") - if host, _, found := strings.Cut(value, ":"); found { - return strings.TrimSpace(host) + if host, port, found := strings.Cut(value, ":"); found { + return strings.TrimSpace(host), strings.TrimSpace(port) } - return strings.TrimSpace(value) + return strings.TrimSpace(value), "" } func (s *Server) requestMilestoneHardware( diff --git a/backend/camera_firmware.go b/backend/camera_firmware.go index eaa0ea5..60fc09d 100644 --- a/backend/camera_firmware.go +++ b/backend/camera_firmware.go @@ -1,4 +1,4 @@ -// backend/camera_firmware.go +// backend\camera_firmware.go package main diff --git a/backend/cors.go b/backend/cors.go index 243e70f..6768348 100644 --- a/backend/cors.go +++ b/backend/cors.go @@ -12,7 +12,7 @@ func (s *Server) cors(next http.Handler) http.Handler { w.Header().Set("Access-Control-Allow-Origin", s.frontendOrigin) w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") } if r.Method == http.MethodOptions { diff --git a/backend/device_events.go b/backend/device_events.go index 99049bd..edac0f8 100644 --- a/backend/device_events.go +++ b/backend/device_events.go @@ -50,68 +50,19 @@ func (s *Server) publishDeviceChangeEvent( return err } - var notification Notification - - err = s.db.QueryRow( + if err := s.createAndPublishNotification( ctx, - ` - INSERT INTO notifications ( - user_id, - type, - title, - message, - visible, - entity_type, - entity_id, - data - ) - VALUES ( - $1, - $2, - $3, - $4, - false, - 'device', - $5, - $6 - ) - RETURNING - id::TEXT, - user_id::TEXT, - type, - title, - message, - entity_type, - COALESCE(entity_id::TEXT, ''), - data, - visible, - read_at, - created_at - `, userID, notificationType, title, message, + "device", deviceID, dataJSON, - ).Scan( - ¬ification.ID, - ¬ification.UserID, - ¬ification.Type, - ¬ification.Title, - ¬ification.Message, - ¬ification.EntityType, - ¬ification.EntityID, - ¬ification.Data, - ¬ification.Visible, - ¬ification.ReadAt, - ¬ification.CreatedAt, - ) - if err != nil { + true, + ); err != nil { return err } - - notificationsBroker.publish(notification) } return rows.Err() diff --git a/backend/devices.go b/backend/devices.go index 5e2aded..fed1ecc 100644 --- a/backend/devices.go +++ b/backend/devices.go @@ -1,4 +1,4 @@ -// backend/devices.go +// backend\devices.go package main @@ -21,6 +21,7 @@ type Device struct { SerialNumber string `json:"serialNumber"` MacAddress string `json:"macAddress"` IPAddress string `json:"ipAddress"` + MilestonePort string `json:"milestonePort"` Location string `json:"location"` LoanStatus string `json:"loanStatus"` IsBaoDevice bool `json:"isBaoDevice"` @@ -69,8 +70,10 @@ type CreateDeviceRequest struct { SerialNumber string `json:"serialNumber"` MacAddress string `json:"macAddress"` IPAddress string `json:"ipAddress"` + MilestonePort string `json:"milestonePort"` FirmwareVersion string `json:"firmwareVersion"` MilestoneDisplayName string `json:"milestoneDisplayName"` + MilestoneEnabled *bool `json:"milestoneEnabled,omitempty"` Location string `json:"location"` LoanStatus string `json:"loanStatus"` IsBaoDevice bool `json:"isBaoDevice"` @@ -80,6 +83,10 @@ type CreateDeviceRequest struct { type UpdateDeviceRequest = CreateDeviceRequest +type UpdateDeviceJournalEntryRequest struct { + Content string `json:"content"` +} + var errDeviceNotFound = errors.New("device does not exist") func (s *Server) handleDevices(w http.ResponseWriter, r *http.Request) { @@ -105,6 +112,7 @@ func (s *Server) handleListDevices(w http.ResponseWriter, r *http.Request) { serial_number, mac_address, COALESCE(ip_address, ''), + COALESCE(milestone_port, ''), location, loan_status, is_bao_device, @@ -142,6 +150,7 @@ func (s *Server) handleListDevices(w http.ResponseWriter, r *http.Request) { &device.SerialNumber, &device.MacAddress, &device.IPAddress, + &device.MilestonePort, &device.Location, &device.LoanStatus, &device.IsBaoDevice, @@ -270,13 +279,14 @@ func (s *Server) handleCreateDevice(w http.ResponseWriter, r *http.Request) { serial_number, mac_address, ip_address, + milestone_port, location, loan_status, is_bao_device, comment, firmware_version ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, inventory_number, @@ -285,6 +295,7 @@ func (s *Server) handleCreateDevice(w http.ResponseWriter, r *http.Request) { serial_number, mac_address, COALESCE(ip_address, ''), + COALESCE(milestone_port, ''), location, loan_status, is_bao_device, @@ -304,6 +315,7 @@ func (s *Server) handleCreateDevice(w http.ResponseWriter, r *http.Request) { input.SerialNumber, input.MacAddress, input.IPAddress, + input.MilestonePort, input.Location, input.LoanStatus, input.IsBaoDevice, @@ -317,6 +329,7 @@ func (s *Server) handleCreateDevice(w http.ResponseWriter, r *http.Request) { &device.SerialNumber, &device.MacAddress, &device.IPAddress, + &device.MilestonePort, &device.Location, &device.LoanStatus, &device.IsBaoDevice, @@ -449,6 +462,11 @@ func (s *Server) handleUpdateDevice(w http.ResponseWriter, r *http.Request) { return } + nextMilestoneEnabled := oldDevice.MilestoneEnabled + if input.MilestoneEnabled != nil { + nextMilestoneEnabled = *input.MilestoneEnabled + } + var device Device err = tx.QueryRow( @@ -462,12 +480,25 @@ func (s *Server) handleUpdateDevice(w http.ResponseWriter, r *http.Request) { serial_number = $5, mac_address = $6, ip_address = $7, - location = $8, - loan_status = $9, - is_bao_device = $10, - comment = $11, - firmware_version = $12, - milestone_display_name = $13, + milestone_port = $8, + location = $9, + loan_status = $10, + is_bao_device = $11, + comment = $12, + firmware_version = $13, + milestone_display_name = $14, + milestone_enabled = $15, + milestone_synced_at = CASE + WHEN COALESCE(milestone_hardware_id, '') <> '' + AND ( + COALESCE(milestone_display_name, '') <> $14 + OR COALESCE(ip_address, '') <> $7 + OR COALESCE(milestone_port, '') <> $8 + OR milestone_enabled <> $15 + ) + THEN now() + ELSE milestone_synced_at + END, updated_at = now() WHERE id = $1 RETURNING @@ -478,6 +509,7 @@ func (s *Server) handleUpdateDevice(w http.ResponseWriter, r *http.Request) { serial_number, mac_address, COALESCE(ip_address, ''), + COALESCE(milestone_port, ''), location, loan_status, is_bao_device, @@ -498,12 +530,14 @@ func (s *Server) handleUpdateDevice(w http.ResponseWriter, r *http.Request) { input.SerialNumber, input.MacAddress, input.IPAddress, + input.MilestonePort, input.Location, input.LoanStatus, input.IsBaoDevice, input.Comment, input.FirmwareVersion, input.MilestoneDisplayName, + nextMilestoneEnabled, ).Scan( &device.ID, &device.InventoryNumber, @@ -512,6 +546,7 @@ func (s *Server) handleUpdateDevice(w http.ResponseWriter, r *http.Request) { &device.SerialNumber, &device.MacAddress, &device.IPAddress, + &device.MilestonePort, &device.Location, &device.LoanStatus, &device.IsBaoDevice, @@ -634,6 +669,7 @@ func normalizeDeviceInput(input *CreateDeviceRequest) { input.SerialNumber = strings.TrimSpace(input.SerialNumber) input.MacAddress = strings.TrimSpace(input.MacAddress) input.IPAddress = strings.TrimSpace(input.IPAddress) + input.MilestonePort = strings.TrimSpace(input.MilestonePort) input.FirmwareVersion = strings.TrimSpace(input.FirmwareVersion) input.MilestoneDisplayName = strings.TrimSpace(input.MilestoneDisplayName) input.Location = strings.TrimSpace(input.Location) diff --git a/backend/feedback.go b/backend/feedback.go index 474ca0c..d4a3b4c 100644 --- a/backend/feedback.go +++ b/backend/feedback.go @@ -164,7 +164,7 @@ func (s *Server) handleCreateFeedback(w http.ResponseWriter, r *http.Request) { "Neues Feedback", formatText("Neues Feedback von %s.", displayFeedbackUserName(feedback)), feedback.ID, - true, + false, map[string]any{ "feedbackId": feedback.ID, "userId": feedback.UserID, @@ -314,7 +314,7 @@ func (s *Server) handleDeleteFeedback(w http.ResponseWriter, r *http.Request) { "Feedback gelöscht", formatText("Feedback von %s wurde gelöscht.", displayFeedbackUserName(deleted)), deleted.ID, - false, + true, map[string]any{ "feedbackId": deleted.ID, "userId": deleted.UserID, @@ -375,70 +375,19 @@ func (s *Server) notifyAdministratorsAboutFeedback( return err } - var notification Notification - - err = s.db.QueryRow( + if err := s.createAndPublishNotification( ctx, - ` - INSERT INTO notifications ( - user_id, - type, - title, - message, - visible, - entity_type, - entity_id, - data - ) - VALUES ( - $1, - $2, - $3, - $4, - true, - 'feedback', - $5, - $6, - $7 - ) - RETURNING - id::TEXT, - user_id::TEXT, - type, - title, - message, - entity_type, - COALESCE(entity_id::TEXT, ''), - data, - visible, - read_at, - created_at - `, adminUserID, notificationType, title, message, - visible, + "feedback", feedbackID, dataJSON, - ).Scan( - ¬ification.ID, - ¬ification.UserID, - ¬ification.Type, - ¬ification.Title, - ¬ification.Message, - ¬ification.EntityType, - ¬ification.EntityID, - ¬ification.Data, - ¬ification.Visible, - ¬ification.ReadAt, - ¬ification.CreatedAt, - ) - if err != nil { + visible, + ); err != nil { return err } - - notificationsBroker.publish(notification) } return adminRows.Err() diff --git a/backend/history.go b/backend/history.go index f60f7e1..6639836 100644 --- a/backend/history.go +++ b/backend/history.go @@ -21,6 +21,8 @@ type DeviceHistoryEntry struct { Action string `json:"action"` Changes []DeviceHistoryChange `json:"changes"` CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + CanEdit bool `json:"canEdit"` } type DeviceHistoryChange struct { @@ -42,6 +44,12 @@ func (s *Server) handleListDeviceHistory(w http.ResponseWriter, r *http.Request) return } + user, ok := userFromContext(r.Context()) + if !ok { + writeError(w, http.StatusUnauthorized, "Unauthorized") + return + } + rows, err := s.db.Query( r.Context(), ` @@ -52,7 +60,8 @@ func (s *Server) handleListDeviceHistory(w http.ResponseWriter, r *http.Request) COALESCE(NULLIF(u.display_name, ''), NULLIF(u.username, ''), u.email, 'Unbekannt'), h.action, h.changes, - h.created_at + h.created_at, + COALESCE(h.updated_at, h.created_at) FROM device_history h LEFT JOIN users u ON u.id = h.user_id WHERE h.device_id = $1 @@ -81,6 +90,7 @@ func (s *Server) handleListDeviceHistory(w http.ResponseWriter, r *http.Request) &entry.Action, &changesRaw, &entry.CreatedAt, + &entry.UpdatedAt, ); err != nil { writeError(w, http.StatusInternalServerError, "Could not read device history") return @@ -93,6 +103,11 @@ func (s *Server) handleListDeviceHistory(w http.ResponseWriter, r *http.Request) } } + entry.CanEdit = + entry.Action == "journal" && + entry.UserID == user.ID && + time.Since(entry.CreatedAt) <= 10*time.Minute + history = append(history, entry) } @@ -239,6 +254,13 @@ func getDeviceForHistory(ctx context.Context, tx pgx.Tx, deviceID string) (Devic model, serial_number, mac_address, + COALESCE(ip_address, ''), + COALESCE(firmware_version, ''), + COALESCE(milestone_recording_server_id, ''), + COALESCE(milestone_hardware_id, ''), + COALESCE(milestone_display_name, ''), + milestone_enabled, + milestone_synced_at, location, loan_status, is_bao_device, @@ -257,6 +279,13 @@ func getDeviceForHistory(ctx context.Context, tx pgx.Tx, deviceID string) (Devic &device.Model, &device.SerialNumber, &device.MacAddress, + &device.IPAddress, + &device.FirmwareVersion, + &device.MilestoneRecordingServerID, + &device.MilestoneHardwareID, + &device.MilestoneDisplayName, + &device.MilestoneEnabled, + &device.MilestoneSyncedAt, &device.Location, &device.LoanStatus, &device.IsBaoDevice, @@ -304,6 +333,190 @@ func getDeviceForHistory(ctx context.Context, tx pgx.Tx, deviceID string) (Devic return device, relatedDeviceIDs, nil } +func (s *Server) handleUpdateDeviceJournalEntry(w http.ResponseWriter, r *http.Request) { + deviceID := strings.TrimSpace(r.PathValue("id")) + historyID := strings.TrimSpace(r.PathValue("historyId")) + + if deviceID == "" { + writeError(w, http.StatusBadRequest, "Geräte-ID fehlt") + return + } + + if historyID == "" { + writeError(w, http.StatusBadRequest, "Journal-ID fehlt") + return + } + + var input UpdateDeviceJournalEntryRequest + + 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()) + + if err := ensureDeviceExists(r.Context(), tx, deviceID); err != nil { + if errors.Is(err, errDeviceNotFound) { + writeError(w, http.StatusNotFound, "Gerät wurde nicht gefunden") + return + } + + writeError(w, http.StatusInternalServerError, "Gerät konnte nicht geladen werden") + return + } + + var changesRaw []byte + var createdAt time.Time + + err = tx.QueryRow( + r.Context(), + ` + SELECT changes, created_at + FROM device_history + WHERE id = $1 + AND device_id = $2 + AND user_id = $3 + AND action = 'journal' + LIMIT 1 + `, + historyID, + deviceID, + 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 []DeviceHistoryChange + + 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 := []DeviceHistoryChange{ + { + 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 device_history + SET + changes = $4::JSONB, + updated_at = now() + WHERE id = $1 + AND device_id = $2 + AND user_id = $3 + AND action = 'journal' + AND created_at >= now() - interval '10 minutes' + `, + historyID, + deviceID, + 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 + } + + if err := s.publishDeviceChangeEvent( + r.Context(), + user.ID, + deviceID, + "device.journal", + "Geräte-Journal aktualisiert", + "Ein Geräte-Journal-Eintrag wurde aktualisiert.", + LogFields{ + "deviceId": deviceID, + "historyId": historyID, + "action": "updated", + }, + ); err != nil { + logError("Device-Journal-SSE-Event konnte nicht gesendet werden", err, LogFields{ + "deviceId": deviceID, + "historyId": historyID, + "type": "device.journal", + }) + } + + writeJSON(w, http.StatusOK, map[string]any{ + "ok": true, + }) +} + func formatHistoryBool(value bool) string { if value { return "Ja" @@ -357,6 +570,17 @@ func buildDeviceUpdateHistoryChanges( changes = appendHistoryChange(changes, "loanStatus", "Verleih-Status", oldDevice.LoanStatus, input.LoanStatus) changes = appendHistoryChange(changes, "isBaoDevice", "BAO-Gerät", formatHistoryBool(oldDevice.IsBaoDevice), formatHistoryBool(input.IsBaoDevice)) changes = appendHistoryChange(changes, "comment", "Kommentar", oldDevice.Comment, input.Comment) + changes = appendHistoryChange(changes, "milestoneDisplayName", "Milestone-Anzeigename", oldDevice.MilestoneDisplayName, input.MilestoneDisplayName) + changes = appendHistoryChange(changes, "ipAddress", "IP-Adresse", oldDevice.IPAddress, input.IPAddress) + if input.MilestoneEnabled != nil { + changes = appendHistoryChange( + changes, + "milestoneEnabled", + "Milestone-Status", + formatHistoryBool(oldDevice.MilestoneEnabled), + formatHistoryBool(*input.MilestoneEnabled), + ) + } oldRelatedDevices, err := formatRelatedDeviceLabels(ctx, tx, oldRelatedDeviceIDs) if err != nil { diff --git a/backend/milestone_api.go b/backend/milestone_api.go new file mode 100644 index 0000000..fa35bf0 --- /dev/null +++ b/backend/milestone_api.go @@ -0,0 +1,614 @@ +// backend\milestone_api.go + +package main + +import ( + "bytes" + "context" + "database/sql" + "encoding/json" + "errors" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/jackc/pgx/v5" +) + +type milestoneRuntimeConfig struct { + Host string + AccessToken string + TokenType string + SkipTLSVerify bool + TokenExpiresAt *time.Time +} + +type milestoneHardwarePatchDevice struct { + ID string + InventoryNumber string + MilestoneHardwareID string + MilestoneDisplayName string + IPAddress string + MilestonePort string + MilestoneEnabled bool +} + +type updateMilestoneHardwareRequest struct { + Name *string `json:"name"` + DisplayName *string `json:"displayName"` + MilestoneDisplayName *string `json:"milestoneDisplayName"` + IPAddress *string `json:"ipAddress"` + Address *string `json:"address"` + Port *string `json:"port"` + MilestonePort *string `json:"milestonePort"` + Enabled *bool `json:"enabled"` +} + +func (s *Server) getMilestoneHardwarePatchDevice( + ctx context.Context, + deviceID string, +) (milestoneHardwarePatchDevice, error) { + var device milestoneHardwarePatchDevice + + err := s.db.QueryRow( + ctx, + ` + SELECT + id::TEXT, + inventory_number, + COALESCE(milestone_hardware_id, ''), + COALESCE(milestone_display_name, ''), + COALESCE(ip_address, ''), + COALESCE(milestone_port, ''), + milestone_enabled + FROM devices + WHERE id = $1 + LIMIT 1 + `, + deviceID, + ).Scan( + &device.ID, + &device.InventoryNumber, + &device.MilestoneHardwareID, + &device.MilestoneDisplayName, + &device.IPAddress, + &device.MilestonePort, + &device.MilestoneEnabled, + ) + + return device, err +} + +func (s *Server) handleUpdateMilestoneHardware(w http.ResponseWriter, r *http.Request) { + user, ok := userFromContext(r.Context()) + if !ok { + writeError(w, http.StatusUnauthorized, "Unauthorized") + return + } + + deviceID := strings.TrimSpace(r.PathValue("id")) + if deviceID == "" { + writeError(w, http.StatusBadRequest, "Geräte-ID fehlt") + return + } + + var input updateMilestoneHardwareRequest + + if err := readJSON(r, &input); err != nil { + writeError(w, http.StatusBadRequest, "Invalid JSON") + return + } + + if input.Name == nil && + input.DisplayName == nil && + input.MilestoneDisplayName == nil && + input.IPAddress == nil && + input.Address == nil && + input.Port == nil && + input.MilestonePort == nil && + input.Enabled == nil { + writeError(w, http.StatusBadRequest, "Keine Milestone-Felder zum Aktualisieren übergeben") + return + } + + device, err := s.getMilestoneHardwarePatchDevice(r.Context(), deviceID) + if errors.Is(err, pgx.ErrNoRows) { + writeError(w, http.StatusNotFound, "Gerät wurde nicht gefunden") + return + } + + if err != nil { + writeError(w, http.StatusInternalServerError, "Gerät konnte nicht geladen werden") + return + } + + if strings.TrimSpace(device.MilestoneHardwareID) == "" { + writeError(w, http.StatusBadRequest, "Dieses Gerät ist kein Milestone-Gerät") + return + } + + nextDisplayName := strings.TrimSpace(device.MilestoneDisplayName) + nextIPAddress := strings.TrimSpace(device.IPAddress) + nextPort := strings.TrimSpace(device.MilestonePort) + nextEnabled := device.MilestoneEnabled + + payload := map[string]any{} + changes := []DeviceHistoryChange{} + + requestedDisplayName := input.Name + if requestedDisplayName == nil { + requestedDisplayName = input.DisplayName + } + if requestedDisplayName == nil { + requestedDisplayName = input.MilestoneDisplayName + } + + if requestedDisplayName != nil { + nextDisplayName = strings.TrimSpace(*requestedDisplayName) + + // Wenn der Client einen Namen sendet, immer an Milestone weitergeben. + payload["name"] = nextDisplayName + + if nextDisplayName != strings.TrimSpace(device.MilestoneDisplayName) { + changes = appendHistoryChange( + changes, + "milestoneDisplayName", + "Milestone-Anzeigename", + device.MilestoneDisplayName, + nextDisplayName, + ) + } + } + + requestedAddress := input.Address + if requestedAddress == nil { + requestedAddress = input.IPAddress + } + + requestedPort := input.Port + if requestedPort == nil { + requestedPort = input.MilestonePort + } + + if requestedAddress != nil || requestedPort != nil { + addressValue := device.IPAddress + if requestedAddress != nil { + addressValue = *requestedAddress + } + + portValue := device.MilestonePort + if requestedPort != nil { + portValue = *requestedPort + } + + nextAddress := normalizeMilestoneHardwareAddress(addressValue, portValue) + nextIPAddress, parsedPort := extractMilestoneHardwareAddressParts(nextAddress) + nextPort = normalizeMilestonePort(portValue) + if nextPort == "" { + nextPort = parsedPort + } + + if nextIPAddress == "" { + nextIPAddress = strings.TrimSpace(addressValue) + } + + // Wenn der Client Adresse oder Port sendet, immer an Milestone weitergeben. + payload["address"] = nextAddress + + if nextIPAddress != strings.TrimSpace(device.IPAddress) { + changes = appendHistoryChange( + changes, + "ipAddress", + "IP-Adresse", + device.IPAddress, + nextIPAddress, + ) + } + + if nextPort != strings.TrimSpace(device.MilestonePort) { + changes = appendHistoryChange( + changes, + "milestonePort", + "Milestone-Port", + device.MilestonePort, + nextPort, + ) + } + } + + if input.Enabled != nil { + nextEnabled = *input.Enabled + + // Wenn der Client den Status sendet, immer an Milestone weitergeben. + payload["enabled"] = nextEnabled + + if nextEnabled != device.MilestoneEnabled { + changes = appendHistoryChange( + changes, + "milestoneEnabled", + "Milestone-Status", + formatHistoryBool(device.MilestoneEnabled), + formatHistoryBool(nextEnabled), + ) + } + } + + if len(payload) == 0 { + writeJSON(w, http.StatusOK, map[string]any{ + "ok": true, + "id": device.ID, + "milestoneDisplayName": device.MilestoneDisplayName, + "ipAddress": device.IPAddress, + "milestonePort": device.MilestonePort, + "milestoneEnabled": device.MilestoneEnabled, + }) + return + } + + config, err := s.getMilestoneRuntimeConfig(r.Context()) + if errors.Is(err, pgx.ErrNoRows) { + writeError(w, http.StatusBadRequest, "Milestone-Einstellungen sind noch nicht hinterlegt") + return + } + + if err != nil { + s.logAndWriteUserError( + w, + r, + user, + http.StatusBadGateway, + "milestone_hardware", + "load_or_refresh_token", + "Milestone-Token konnte nicht geladen oder erneuert werden", + err, + LogFields{ + "deviceId": device.ID, + "inventoryNumber": device.InventoryNumber, + "milestoneHardwareId": device.MilestoneHardwareID, + }, + ) + return + } + + if strings.TrimSpace(config.Host) == "" { + writeError(w, http.StatusBadRequest, "Milestone Host fehlt") + return + } + + if strings.TrimSpace(config.AccessToken) == "" { + writeError(w, http.StatusBadRequest, "Milestone-Token fehlt und konnte nicht automatisch erneuert werden") + return + } + + if err := patchMilestoneHardware( + r.Context(), + config, + device.MilestoneHardwareID, + payload, + ); err != nil { + s.logAndWriteUserError( + w, + r, + user, + http.StatusBadGateway, + "milestone_hardware", + "patch_hardware", + "Milestone-Gerät konnte nicht aktualisiert werden", + err, + LogFields{ + "deviceId": device.ID, + "inventoryNumber": device.InventoryNumber, + "milestoneHardwareId": device.MilestoneHardwareID, + "payload": payload, + }, + ) + return + } + + tx, err := s.db.Begin(r.Context()) + if err != nil { + writeError(w, http.StatusInternalServerError, "Milestone-Gerät konnte lokal nicht aktualisiert werden") + return + } + defer tx.Rollback(r.Context()) + + _, err = tx.Exec( + r.Context(), + ` + UPDATE devices + SET + milestone_display_name = $2, + ip_address = $3, + milestone_port = $4, + milestone_enabled = $5, + milestone_synced_at = now(), + updated_at = now() + WHERE id = $1 + `, + device.ID, + nextDisplayName, + nextIPAddress, + nextPort, + nextEnabled, + ) + if err != nil { + writeError(w, http.StatusInternalServerError, "Milestone-Gerät konnte lokal nicht aktualisiert werden") + return + } + + if len(changes) > 0 { + if err := recordDeviceHistory(r.Context(), tx, device.ID, user, "updated", changes); err != nil { + writeError(w, http.StatusInternalServerError, "Geräteverlauf konnte nicht gespeichert werden") + return + } + } + + if err := tx.Commit(r.Context()); err != nil { + writeError(w, http.StatusInternalServerError, "Milestone-Gerät konnte lokal nicht aktualisiert werden") + return + } + + if err := s.publishDeviceChangeEvent( + r.Context(), + user.ID, + device.ID, + "device.milestone.hardware", + "Milestone-Gerät aktualisiert", + formatText("Gerät %s wurde in Milestone aktualisiert.", device.InventoryNumber), + LogFields{ + "inventoryNumber": device.InventoryNumber, + "milestoneHardwareId": device.MilestoneHardwareID, + "payload": payload, + }, + ); err != nil { + logError("Device-SSE-Event konnte nicht gesendet werden", err, LogFields{ + "deviceId": device.ID, + "type": "device.milestone.hardware", + }) + } + + writeJSON(w, http.StatusOK, map[string]any{ + "ok": true, + "id": device.ID, + "milestoneDisplayName": nextDisplayName, + "ipAddress": nextIPAddress, + "milestonePort": nextPort, + "milestoneEnabled": nextEnabled, + }) +} + +func normalizeMilestoneHardwareAddress(value string, port string) string { + value = strings.TrimSpace(value) + port = normalizeMilestonePort(port) + + if value == "" || value == "-" { + return "" + } + + if !strings.HasPrefix(value, "http://") && !strings.HasPrefix(value, "https://") { + value = "http://" + strings.TrimRight(value, "/") + } + + parsedURL, err := url.Parse(value) + if err != nil || parsedURL.Hostname() == "" { + value = strings.TrimRight(value, "/") + if port != "" && !strings.Contains(value, ":") { + value += ":" + port + } + + return value + "/" + } + + scheme := strings.TrimSpace(parsedURL.Scheme) + if scheme == "" { + scheme = "http" + } + + host := parsedURL.Hostname() + if port == "" { + port = parsedURL.Port() + } + + hostPort := host + if port != "" { + hostPort += ":" + port + } + + return scheme + "://" + hostPort + "/" +} + +func normalizeMilestonePort(value string) string { + value = strings.TrimSpace(value) + value = strings.TrimPrefix(value, ":") + + if value == "-" { + return "" + } + + return value +} + +const milestoneTokenRefreshBeforeExpiry = 2 * time.Minute + +func (s *Server) getMilestoneRuntimeConfig( + ctx context.Context, +) (milestoneRuntimeConfig, error) { + config, err := s.loadMilestoneRuntimeConfig(ctx) + if err != nil { + return config, err + } + + if shouldRefreshMilestoneToken(config) { + if err := s.refreshMilestoneRuntimeToken(ctx); err != nil { + return config, err + } + + return s.loadMilestoneRuntimeConfig(ctx) + } + + return config, nil +} + +func shouldRefreshMilestoneToken(config milestoneRuntimeConfig) bool { + if strings.TrimSpace(config.AccessToken) == "" { + return true + } + + if config.TokenExpiresAt == nil { + return true + } + + return time.Now().After(config.TokenExpiresAt.Add(-milestoneTokenRefreshBeforeExpiry)) +} + +func (s *Server) loadMilestoneRuntimeConfig( + ctx context.Context, +) (milestoneRuntimeConfig, error) { + var config milestoneRuntimeConfig + var tokenExpiresAt sql.NullTime + + err := s.db.QueryRow( + ctx, + ` + SELECT + host, + COALESCE(pgp_sym_decrypt(access_token_encrypted, $1), ''), + COALESCE(token_type, ''), + skip_tls_verify, + token_expires_at + FROM milestone_settings + WHERE id = 1 + LIMIT 1 + `, + s.milestoneEncryptionKey(), + ).Scan( + &config.Host, + &config.AccessToken, + &config.TokenType, + &config.SkipTLSVerify, + &tokenExpiresAt, + ) + if err != nil { + return config, err + } + + if tokenExpiresAt.Valid { + config.TokenExpiresAt = &tokenExpiresAt.Time + } + + return config, nil +} + +func (s *Server) refreshMilestoneRuntimeToken(ctx context.Context) error { + credentials, err := s.getMilestoneCredentials(ctx) + if err != nil { + return err + } + + if credentials.Host == "" || credentials.Username == "" || credentials.Password == "" { + return errors.New("Milestone-Zugangsdaten sind unvollständig") + } + + tokenResponse, err := s.requestMilestoneToken(ctx, credentials) + if err != nil { + return err + } + + accessToken := strings.TrimSpace(tokenResponse.AccessToken) + if accessToken == "" { + return errors.New("Milestone hat keinen Access-Token zurückgegeben") + } + + tokenType := strings.TrimSpace(tokenResponse.TokenType) + if tokenType == "" { + tokenType = "Bearer" + } + + expiresIn := tokenResponse.ExpiresIn + if expiresIn <= 0 { + expiresIn = 3600 + } + + tokenExpiresAt := time.Now().Add(time.Duration(expiresIn) * time.Second) + + _, err = s.db.Exec( + ctx, + ` + UPDATE milestone_settings + SET + access_token_encrypted = pgp_sym_encrypt($1, $2), + token_type = $3, + token_expires_at = $4, + token_updated_at = now(), + updated_at = now() + WHERE id = 1 + `, + accessToken, + s.milestoneEncryptionKey(), + tokenType, + tokenExpiresAt, + ) + + return err +} + +func patchMilestoneHardware( + ctx context.Context, + config milestoneRuntimeConfig, + hardwareID string, + payload any, +) error { + body, err := json.Marshal(payload) + if err != nil { + return err + } + + requestURL := strings.TrimRight(config.Host, "/") + + "/API/rest/v1/hardware/" + + url.PathEscape(hardwareID) + + request, err := http.NewRequestWithContext( + ctx, + http.MethodPatch, + requestURL, + bytes.NewReader(body), + ) + if err != nil { + return err + } + + tokenType := strings.TrimSpace(config.TokenType) + if tokenType == "" { + tokenType = "Bearer" + } + + request.Header.Set("Authorization", tokenType+" "+config.AccessToken) + request.Header.Set("Accept", "application/json") + request.Header.Set("Content-Type", "application/json") + + response, err := milestoneHTTPClient(config.SkipTLSVerify).Do(request) + if err != nil { + return err + } + defer response.Body.Close() + + responseBody, _ := io.ReadAll(response.Body) + + if response.StatusCode < 200 || response.StatusCode >= 300 { + return newAppErrorf( + "milestone hardware patch failed", + LogFields{ + "statusCode": response.StatusCode, + "responseBody": string(responseBody), + "hardwareId": hardwareID, + }, + "milestone hardware patch failed: status=%d body=%s", + response.StatusCode, + string(responseBody), + ) + } + + return nil +} diff --git a/backend/milestone_devices.go b/backend/milestone_devices.go index ffa12ab..6d4c224 100644 --- a/backend/milestone_devices.go +++ b/backend/milestone_devices.go @@ -4,7 +4,6 @@ package main import ( "context" - "fmt" "strings" "unicode" @@ -28,6 +27,7 @@ func syncMilestoneHardwareDevices( hardwareDevice.SerialNumber = strings.TrimSpace(hardwareDevice.SerialNumber) hardwareDevice.MacAddress = strings.TrimSpace(hardwareDevice.MacAddress) hardwareDevice.IPAddress = strings.TrimSpace(hardwareDevice.IPAddress) + hardwareDevice.Port = strings.TrimSpace(hardwareDevice.Port) hardwareDevice.FirmwareVersion = strings.TrimSpace(hardwareDevice.FirmwareVersion) if hardwareDevice.HardwareID == "" { @@ -78,6 +78,7 @@ func createOrUpdateMilestoneHardwareDevice( serial_number, mac_address, ip_address, + milestone_port, location, loan_status, is_bao_device, @@ -96,10 +97,11 @@ func createOrUpdateMilestoneHardwareDevice( $4, $5, $6, + $7, '', 'Verfügbar', false, - $7, + '', $8, $9, $10, @@ -116,8 +118,7 @@ func createOrUpdateMilestoneHardwareDevice( serial_number = EXCLUDED.serial_number, mac_address = EXCLUDED.mac_address, ip_address = EXCLUDED.ip_address, - location = EXCLUDED.location, - comment = EXCLUDED.comment, + milestone_port = EXCLUDED.milestone_port, firmware_version = EXCLUDED.firmware_version, milestone_recording_server_id = EXCLUDED.milestone_recording_server_id, milestone_display_name = EXCLUDED.milestone_display_name, @@ -131,7 +132,7 @@ func createOrUpdateMilestoneHardwareDevice( hardwareDevice.SerialNumber, hardwareDevice.MacAddress, hardwareDevice.IPAddress, - buildMilestoneDeviceComment(hardwareDevice), + hardwareDevice.Port, hardwareDevice.FirmwareVersion, hardwareDevice.RecordingServerID, hardwareDevice.HardwareID, @@ -324,31 +325,3 @@ func buildGeneratedMilestoneInventoryNumber(hardwareID string) string { return "MS-" + value } - -func buildMilestoneDeviceComment(hardwareDevice milestoneHardwareDevice) string { - parts := []string{} - - if strings.TrimSpace(hardwareDevice.DisplayName) != "" { - parts = append(parts, strings.TrimSpace(hardwareDevice.DisplayName)) - } - - if strings.TrimSpace(hardwareDevice.DeviceName) != "" && - strings.TrimSpace(hardwareDevice.DeviceName) != strings.TrimSpace(hardwareDevice.DisplayName) { - parts = append(parts, "DeviceName: "+strings.TrimSpace(hardwareDevice.DeviceName)) - } - - if hardwareDevice.Enabled { - parts = append(parts, "Status: aktiviert") - } else { - parts = append(parts, "Status: deaktiviert") - } - - if len(parts) == 0 { - return "Automatisch aus Milestone synchronisiert." - } - - return fmt.Sprintf( - "Automatisch aus Milestone synchronisiert: %s", - strings.Join(parts, " · "), - ) -} diff --git a/backend/milestone_hardware.go b/backend/milestone_hardware.go deleted file mode 100644 index 55d9c96..0000000 --- a/backend/milestone_hardware.go +++ /dev/null @@ -1,338 +0,0 @@ -// backend/milestone_hardware.go - -package main - -import ( - "bytes" - "context" - "database/sql" - "encoding/json" - "errors" - "io" - "net/http" - "net/url" - "strings" - "time" - - "github.com/jackc/pgx/v5" -) - -type milestoneRuntimeConfig struct { - Host string - AccessToken string - TokenType string - SkipTLSVerify bool - TokenExpiresAt *time.Time -} - -type milestoneToggleDevice struct { - ID string - InventoryNumber string - MilestoneHardwareID string - MilestoneEnabled bool -} - -func (s *Server) handleToggleMilestoneDeviceStatus(w http.ResponseWriter, r *http.Request) { - user, ok := userFromContext(r.Context()) - if !ok { - writeError(w, http.StatusUnauthorized, "Unauthorized") - return - } - - deviceID := strings.TrimSpace(r.PathValue("id")) - if deviceID == "" { - writeError(w, http.StatusBadRequest, "Geräte-ID fehlt") - return - } - - device, err := s.getMilestoneToggleDevice(r.Context(), deviceID) - if errors.Is(err, pgx.ErrNoRows) { - writeError(w, http.StatusNotFound, "Gerät wurde nicht gefunden") - return - } - - if err != nil { - writeError(w, http.StatusInternalServerError, "Gerät konnte nicht geladen werden") - return - } - - if device.MilestoneHardwareID == "" { - writeError(w, http.StatusBadRequest, "Dieses Gerät ist kein Milestone-Gerät") - return - } - - config, err := s.getMilestoneRuntimeConfig(r.Context()) - if errors.Is(err, pgx.ErrNoRows) { - writeError(w, http.StatusBadRequest, "Milestone-Einstellungen sind noch nicht hinterlegt") - return - } - - if err != nil { - writeError(w, http.StatusInternalServerError, "Milestone-Einstellungen konnten nicht geladen werden") - return - } - - if config.Host == "" || config.AccessToken == "" { - writeError(w, http.StatusBadRequest, "Milestone Host oder Token fehlt") - return - } - - if config.TokenExpiresAt != nil && time.Now().After(config.TokenExpiresAt.Add(-30*time.Second)) { - writeError(w, http.StatusBadRequest, "Milestone-Token ist abgelaufen. Bitte Token neu abrufen.") - return - } - - nextEnabled := !device.MilestoneEnabled - - if err := patchMilestoneHardware( - r.Context(), - config, - device.MilestoneHardwareID, - map[string]bool{ - "enabled": nextEnabled, - }, - ); err != nil { - s.logAndWriteUserError( - w, - r, - user, - http.StatusBadGateway, - "milestone_hardware", - "patch_hardware", - "Milestone-Status konnte nicht geändert werden", - err, - LogFields{ - "deviceId": device.ID, - "inventoryNumber": device.InventoryNumber, - "milestoneHardwareId": device.MilestoneHardwareID, - "currentEnabled": device.MilestoneEnabled, - "nextEnabled": nextEnabled, - "payload": LogFields{ - "enabled": nextEnabled, - }, - }, - ) - return - } - - _, err = s.db.Exec( - r.Context(), - ` - UPDATE devices - SET - milestone_enabled = $2, - milestone_synced_at = now(), - updated_at = now() - WHERE id = $1 - `, - device.ID, - nextEnabled, - ) - if err != nil { - s.logAndWriteUserError( - w, - r, - user, - http.StatusInternalServerError, - "milestone_hardware", - "update_local_device", - "Milestone-Status konnte lokal nicht aktualisiert werden", - err, - LogFields{ - "deviceId": device.ID, - "inventoryNumber": device.InventoryNumber, - "milestoneHardwareId": device.MilestoneHardwareID, - "nextEnabled": nextEnabled, - }, - ) - return - } - - oldValue := "Aktiviert" - newValue := "Deaktiviert" - - if nextEnabled { - oldValue = "Deaktiviert" - newValue = "Aktiviert" - } - - changes, _ := json.Marshal([]map[string]string{ - { - "field": "milestoneEnabled", - "label": "Milestone-Status", - "oldValue": oldValue, - "newValue": newValue, - }, - }) - - _, _ = s.db.Exec( - r.Context(), - ` - INSERT INTO device_history ( - device_id, - user_id, - action, - changes - ) - VALUES ($1, $2, 'updated', $3) - `, - device.ID, - user.ID, - changes, - ) - - if err := s.publishDeviceChangeEvent( - r.Context(), - user.ID, - device.ID, - "device.milestone.status", - "Milestone-Status geändert", - formatText( - "Gerät %s wurde in Milestone %s.", - device.InventoryNumber, - strings.ToLower(newValue), - ), - LogFields{ - "inventoryNumber": device.InventoryNumber, - "milestoneHardwareId": device.MilestoneHardwareID, - "milestoneEnabled": nextEnabled, - }, - ); err != nil { - logError("Device-SSE-Event konnte nicht gesendet werden", err, LogFields{ - "deviceId": device.ID, - "type": "device.milestone.status", - }) - } - - writeJSON(w, http.StatusOK, map[string]any{ - "ok": true, - "id": device.ID, - "milestoneEnabled": nextEnabled, - }) -} - -func (s *Server) getMilestoneToggleDevice( - ctx context.Context, - deviceID string, -) (milestoneToggleDevice, error) { - var device milestoneToggleDevice - - err := s.db.QueryRow( - ctx, - ` - SELECT - id::TEXT, - inventory_number, - COALESCE(milestone_hardware_id, ''), - milestone_enabled - FROM devices - WHERE id = $1 - LIMIT 1 - `, - deviceID, - ).Scan( - &device.ID, - &device.InventoryNumber, - &device.MilestoneHardwareID, - &device.MilestoneEnabled, - ) - - return device, err -} - -func (s *Server) getMilestoneRuntimeConfig( - ctx context.Context, -) (milestoneRuntimeConfig, error) { - var config milestoneRuntimeConfig - var tokenExpiresAt sql.NullTime - - err := s.db.QueryRow( - ctx, - ` - SELECT - host, - COALESCE(pgp_sym_decrypt(access_token_encrypted, $1), ''), - COALESCE(token_type, ''), - skip_tls_verify, - token_expires_at - FROM milestone_settings - WHERE id = 1 - LIMIT 1 - `, - s.milestoneEncryptionKey(), - ).Scan( - &config.Host, - &config.AccessToken, - &config.TokenType, - &config.SkipTLSVerify, - &tokenExpiresAt, - ) - if err != nil { - return config, err - } - - if tokenExpiresAt.Valid { - config.TokenExpiresAt = &tokenExpiresAt.Time - } - - return config, nil -} - -func patchMilestoneHardware( - ctx context.Context, - config milestoneRuntimeConfig, - hardwareID string, - payload any, -) error { - body, err := json.Marshal(payload) - if err != nil { - return err - } - - requestURL := strings.TrimRight(config.Host, "/") + - "/API/rest/v1/hardware/" + - url.PathEscape(hardwareID) - - request, err := http.NewRequestWithContext( - ctx, - http.MethodPatch, - requestURL, - bytes.NewReader(body), - ) - if err != nil { - return err - } - - tokenType := strings.TrimSpace(config.TokenType) - if tokenType == "" { - tokenType = "Bearer" - } - - request.Header.Set("Authorization", tokenType+" "+config.AccessToken) - request.Header.Set("Accept", "application/json") - request.Header.Set("Content-Type", "application/json") - - response, err := milestoneHTTPClient(config.SkipTLSVerify).Do(request) - if err != nil { - return err - } - defer response.Body.Close() - - responseBody, _ := io.ReadAll(response.Body) - - if response.StatusCode < 200 || response.StatusCode >= 300 { - return newAppErrorf( - "milestone hardware patch failed", - LogFields{ - "statusCode": response.StatusCode, - "responseBody": string(responseBody), - "hardwareId": hardwareID, - }, - "milestone hardware patch failed: status=%d body=%s", - response.StatusCode, - string(responseBody), - ) - } - - return nil -} diff --git a/backend/notification_preferences.go b/backend/notification_preferences.go index 486ad5e..e0fcfd1 100644 --- a/backend/notification_preferences.go +++ b/backend/notification_preferences.go @@ -1,29 +1,33 @@ -// backend\notification_preferences.go +// backend/notification_preferences.go package main import ( + "context" "errors" "net/http" + "strings" "github.com/jackc/pgx/v5" ) type notificationPreferencesResponse struct { - InAppEnabled bool `json:"inAppEnabled"` - SoundEnabled bool `json:"soundEnabled"` - EmailEnabled bool `json:"emailEnabled"` - OperationUpdates bool `json:"operationUpdates"` - OperationJournals bool `json:"operationJournals"` - DeviceUpdates bool `json:"deviceUpdates"` - DeviceJournals bool `json:"deviceJournals"` - SystemMessages bool `json:"systemMessages"` + InAppEnabled bool `json:"inAppEnabled"` + SoundEnabled bool `json:"soundEnabled"` + SoundName string `json:"soundName"` + EmailEnabled bool `json:"emailEnabled"` + OperationUpdates bool `json:"operationUpdates"` + OperationJournals bool `json:"operationJournals"` + DeviceUpdates bool `json:"deviceUpdates"` + DeviceJournals bool `json:"deviceJournals"` + SystemMessages bool `json:"systemMessages"` } func defaultNotificationPreferences() notificationPreferencesResponse { return notificationPreferencesResponse{ InAppEnabled: true, SoundEnabled: false, + SoundName: "default", EmailEnabled: false, OperationUpdates: true, OperationJournals: true, @@ -33,6 +37,17 @@ func defaultNotificationPreferences() notificationPreferencesResponse { } } +func normalizeNotificationSoundName(value string) string { + value = strings.TrimSpace(strings.ToLower(value)) + + switch value { + case "default", "chime", "soft", "alert", "success": + return value + default: + return "default" + } +} + func (s *Server) handleGetNotificationPreferences(w http.ResponseWriter, r *http.Request) { user, ok := userFromContext(r.Context()) if !ok { @@ -40,7 +55,7 @@ func (s *Server) handleGetNotificationPreferences(w http.ResponseWriter, r *http return } - settings, err := s.getNotificationPreferences(r, user.ID) + settings, err := s.getNotificationPreferences(r.Context(), user.ID) if errors.Is(err, pgx.ErrNoRows) { writeJSON(w, http.StatusOK, map[string]any{ "settings": defaultNotificationPreferences(), @@ -72,6 +87,8 @@ func (s *Server) handleUpdateNotificationPreferences(w http.ResponseWriter, r *h return } + input.SoundName = normalizeNotificationSoundName(input.SoundName) + _, err := s.db.Exec( r.Context(), ` @@ -79,6 +96,7 @@ func (s *Server) handleUpdateNotificationPreferences(w http.ResponseWriter, r *h user_id, in_app_enabled, sound_enabled, + sound_name, email_enabled, operation_updates, operation_journals, @@ -86,11 +104,12 @@ func (s *Server) handleUpdateNotificationPreferences(w http.ResponseWriter, r *h device_journals, system_messages ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (user_id) DO UPDATE SET in_app_enabled = EXCLUDED.in_app_enabled, sound_enabled = EXCLUDED.sound_enabled, + sound_name = EXCLUDED.sound_name, email_enabled = EXCLUDED.email_enabled, operation_updates = EXCLUDED.operation_updates, operation_journals = EXCLUDED.operation_journals, @@ -102,6 +121,7 @@ func (s *Server) handleUpdateNotificationPreferences(w http.ResponseWriter, r *h user.ID, input.InAppEnabled, input.SoundEnabled, + input.SoundName, input.EmailEnabled, input.OperationUpdates, input.OperationJournals, @@ -114,7 +134,7 @@ func (s *Server) handleUpdateNotificationPreferences(w http.ResponseWriter, r *h return } - settings, err := s.getNotificationPreferences(r, user.ID) + settings, err := s.getNotificationPreferences(r.Context(), user.ID) if err != nil { writeError(w, http.StatusInternalServerError, "Benachrichtigungseinstellungen wurden gespeichert, konnten aber nicht neu geladen werden") return @@ -125,15 +145,16 @@ func (s *Server) handleUpdateNotificationPreferences(w http.ResponseWriter, r *h }) } -func (s *Server) getNotificationPreferences(r *http.Request, userID string) (notificationPreferencesResponse, error) { +func (s *Server) getNotificationPreferences(ctx context.Context, userID string) (notificationPreferencesResponse, error) { settings := defaultNotificationPreferences() err := s.db.QueryRow( - r.Context(), + ctx, ` SELECT in_app_enabled, sound_enabled, + COALESCE(sound_name, 'default'), email_enabled, operation_updates, operation_journals, @@ -148,6 +169,7 @@ func (s *Server) getNotificationPreferences(r *http.Request, userID string) (not ).Scan( &settings.InAppEnabled, &settings.SoundEnabled, + &settings.SoundName, &settings.EmailEnabled, &settings.OperationUpdates, &settings.OperationJournals, @@ -156,5 +178,16 @@ func (s *Server) getNotificationPreferences(r *http.Request, userID string) (not &settings.SystemMessages, ) + settings.SoundName = normalizeNotificationSoundName(settings.SoundName) + + return settings, err +} + +func (s *Server) getNotificationPreferencesOrDefault(ctx context.Context, userID string) (notificationPreferencesResponse, error) { + settings, err := s.getNotificationPreferences(ctx, userID) + if errors.Is(err, pgx.ErrNoRows) { + return defaultNotificationPreferences(), nil + } + return settings, err } diff --git a/backend/notifications.go b/backend/notifications.go index 2b7040b..7d52c01 100644 --- a/backend/notifications.go +++ b/backend/notifications.go @@ -1,4 +1,4 @@ -// backend\notifications.go +// backend/notifications.go package main @@ -22,7 +22,8 @@ type Notification struct { EntityType string `json:"entityType"` EntityID string `json:"entityId"` Data json.RawMessage `json:"data"` - Visible bool `json:"visible"` + Silent bool `json:"silent"` + SoundName string `json:"soundName,omitempty"` ReadAt *time.Time `json:"readAt"` CreatedAt time.Time `json:"createdAt"` } @@ -77,6 +78,151 @@ func (b *notificationBroker) publish(notification Notification) { } } +func (b *notificationBroker) publishToAll(notification Notification) { + b.mu.RLock() + defer b.mu.RUnlock() + + for client := range b.clients { + select { + case client.ch <- notification: + default: + } + } +} + +func shouldDeliverNotification( + preferences notificationPreferencesResponse, + notificationType string, + entityType string, +) bool { + if !preferences.InAppEnabled { + return false + } + + switch { + case notificationType == "operation.journal": + return preferences.OperationJournals + + case strings.HasPrefix(notificationType, "operation.") || entityType == "operation": + return preferences.OperationUpdates + + case notificationType == "device.journal": + return preferences.DeviceJournals + + case strings.HasPrefix(notificationType, "device.") || entityType == "device": + return preferences.DeviceUpdates + + case strings.HasPrefix(notificationType, "system.") || entityType == "system": + return preferences.SystemMessages + + default: + return true + } +} + +func notificationSoundName(preferences notificationPreferencesResponse) string { + if !preferences.SoundEnabled { + return "" + } + + return normalizeNotificationSoundName(preferences.SoundName) +} + +func (s *Server) createAndPublishNotification( + ctx context.Context, + recipientID string, + notificationType string, + title string, + message string, + entityType string, + entityID string, + dataJSON []byte, + silent bool, +) error { + preferences, err := s.getNotificationPreferencesOrDefault(ctx, recipientID) + if err != nil { + return err + } + + // Sichtbare Benachrichtigungen respektieren die User-Preferences. + // Stille Events werden immer erzeugt, weil sie das UI synchronisieren. + if !silent && !shouldDeliverNotification(preferences, notificationType, entityType) { + return nil + } + + var notification Notification + var rawData []byte + + err = s.db.QueryRow( + ctx, + ` + INSERT INTO notifications ( + user_id, + type, + title, + message, + entity_type, + entity_id, + data, + silent + ) + VALUES ( + $1, + $2, + $3, + $4, + $5, + NULLIF($6, '')::UUID, + $7::JSONB, + $8 + ) + RETURNING + id::TEXT, + user_id::TEXT, + type, + title, + message, + entity_type, + COALESCE(entity_id::TEXT, ''), + data, + silent, + created_at + `, + recipientID, + notificationType, + title, + message, + entityType, + entityID, + string(dataJSON), + silent, + ).Scan( + ¬ification.ID, + ¬ification.UserID, + ¬ification.Type, + ¬ification.Title, + ¬ification.Message, + ¬ification.EntityType, + ¬ification.EntityID, + &rawData, + ¬ification.Silent, + ¬ification.CreatedAt, + ) + if err != nil { + return err + } + + notification.Data = json.RawMessage(rawData) + + if !notification.Silent { + notification.SoundName = notificationSoundName(preferences) + } + + notificationsBroker.publish(notification) + + return nil +} + func (s *Server) handleNotificationEvents(w http.ResponseWriter, r *http.Request) { user, ok := userFromContext(r.Context()) if !ok { @@ -115,12 +261,14 @@ func (s *Server) handleNotificationEvents(w http.ResponseWriter, r *http.Request eventName := "notification" - if !notification.Visible { + if notification.Silent { switch notification.EntityType { case "operation": eventName = "operation-event" case "device": eventName = "device-event" + case "user": + eventName = "user-event" default: eventName = "notification-event" } @@ -152,12 +300,12 @@ func (s *Server) handleListNotifications(w http.ResponseWriter, r *http.Request) entity_type, COALESCE(entity_id::TEXT, ''), data, - visible, + silent, read_at, created_at FROM notifications WHERE user_id = $1 - AND visible = true + AND silent = false ORDER BY created_at DESC LIMIT 50 `, @@ -186,7 +334,7 @@ func (s *Server) handleListNotifications(w http.ResponseWriter, r *http.Request) ¬ification.EntityType, ¬ification.EntityID, &data, - ¬ification.Visible, + ¬ification.Silent, &readAt, ¬ification.CreatedAt, ); err != nil { @@ -307,76 +455,25 @@ func (s *Server) createOperationNotification( return err } - var notification Notification - var rawData []byte - - err := s.db.QueryRow( + if err := s.createAndPublishNotification( ctx, - ` - INSERT INTO notifications ( - user_id, - type, - title, - message, - entity_type, - entity_id, - data, - visible - ) - VALUES ( - $1, - $2, - $3, - $4, - 'operation', - $5, - $6::JSONB, - true - ) - RETURNING - id::TEXT, - user_id::TEXT, - type, - title, - message, - entity_type, - COALESCE(entity_id::TEXT, ''), - data, - visible, - created_at - `, recipientID, notificationType, title, message, + "operation", operation.ID, - string(dataJSON), - ).Scan( - ¬ification.ID, - ¬ification.UserID, - ¬ification.Type, - ¬ification.Title, - ¬ification.Message, - ¬ification.EntityType, - ¬ification.EntityID, - &rawData, - ¬ification.Visible, - ¬ification.CreatedAt, - ) - - if err != nil { + dataJSON, + false, + ); err != nil { return err } - - notification.Data = json.RawMessage(rawData) - - notificationsBroker.publish(notification) } return rows.Err() } -func (s *Server) createOperationUpdateEvent( +func (s *Server) createOperationCreatedEvent( ctx context.Context, actor User, operation Operation, @@ -390,7 +487,7 @@ func (s *Server) createOperationUpdateEvent( rows, err := s.db.Query( ctx, ` - SELECT id + SELECT id::TEXT FROM users WHERE id <> $1 AND ( @@ -412,66 +509,73 @@ func (s *Server) createOperationUpdateEvent( return err } - var notification Notification - var rawData []byte - - err := s.db.QueryRow( + if err := s.createAndPublishNotification( ctx, - ` - INSERT INTO notifications ( - user_id, - type, - title, - message, - entity_type, - entity_id, - data, - visible - ) - VALUES ( - $1, - 'operation.updated', - 'Einsatz geändert', - '', - 'operation', - $2, - $3::JSONB, - false - ) - RETURNING - id::TEXT, - user_id::TEXT, - type, - title, - message, - entity_type, - COALESCE(entity_id::TEXT, ''), - data, - visible, - created_at - `, recipientID, + "operation.created", + "Einsatz erstellt", + "", + "operation", operation.ID, - string(dataJSON), - ).Scan( - ¬ification.ID, - ¬ification.UserID, - ¬ification.Type, - ¬ification.Title, - ¬ification.Message, - ¬ification.EntityType, - ¬ification.EntityID, - &rawData, - ¬ification.Visible, - ¬ification.CreatedAt, - ) + dataJSON, + true, + ); err != nil { + return err + } + } - if err != nil { + return rows.Err() +} + +func (s *Server) createOperationUpdateEvent( + ctx context.Context, + actor User, + operation Operation, + data map[string]any, +) error { + dataJSON, err := json.Marshal(data) + if err != nil { + return err + } + + rows, err := s.db.Query( + ctx, + ` + SELECT id::TEXT + FROM users + WHERE id <> $1 + AND ( + 'admin' = ANY(rights) + OR 'operations:read' = ANY(rights) + ) + `, + actor.ID, + ) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var recipientID string + + if err := rows.Scan(&recipientID); err != nil { return err } - notification.Data = json.RawMessage(rawData) - notificationsBroker.publish(notification) + if err := s.createAndPublishNotification( + ctx, + recipientID, + "operation.updated", + "Einsatz geändert", + "", + "operation", + operation.ID, + dataJSON, + true, + ); err != nil { + return err + } } return rows.Err() @@ -491,7 +595,7 @@ func (s *Server) createOperationJournalEvent( rows, err := s.db.Query( ctx, ` - SELECT id + SELECT id::TEXT FROM users WHERE id <> $1 `, @@ -504,70 +608,24 @@ func (s *Server) createOperationJournalEvent( for rows.Next() { var recipientID string + if err := rows.Scan(&recipientID); err != nil { return err } - var notification Notification - var rawData []byte - - err := s.db.QueryRow( + if err := s.createAndPublishNotification( ctx, - ` - INSERT INTO notifications ( - user_id, - type, - title, - message, - entity_type, - entity_id, - data, - visible - ) - VALUES ( - $1, - 'operation.journal', - 'Journal aktualisiert', - 'Ein Journal-Eintrag wurde aktualisiert.', - 'operation', - $2, - $3::JSONB, - false - ) - RETURNING - id, - user_id::TEXT, - type, - title, - message, - entity_type, - COALESCE(entity_id::TEXT, ''), - data, - visible, - created_at - `, recipientID, + "operation.journal", + "Journal aktualisiert", + "Ein Journal-Eintrag wurde aktualisiert.", + "operation", operation.ID, - string(dataJSON), - ).Scan( - ¬ification.ID, - ¬ification.UserID, - ¬ification.Type, - ¬ification.Title, - ¬ification.Message, - ¬ification.EntityType, - ¬ification.EntityID, - &rawData, - ¬ification.Visible, - ¬ification.CreatedAt, - ) - - if err != nil { + dataJSON, + true, + ); err != nil { return err } - - notification.Data = json.RawMessage(rawData) - notificationsBroker.publish(notification) } return rows.Err() @@ -587,7 +645,7 @@ func (s *Server) createDeviceJournalEvent( rows, err := s.db.Query( ctx, ` - SELECT id + SELECT id::TEXT FROM users WHERE id <> $1 AND ( @@ -604,70 +662,24 @@ func (s *Server) createDeviceJournalEvent( for rows.Next() { var recipientID string + if err := rows.Scan(&recipientID); err != nil { return err } - var notification Notification - var rawData []byte - - err := s.db.QueryRow( + if err := s.createAndPublishNotification( ctx, - ` - INSERT INTO notifications ( - user_id, - type, - title, - message, - entity_type, - entity_id, - data, - visible - ) - VALUES ( - $1, - 'device.journal', - 'Geräte-Journal aktualisiert', - 'Ein Geräte-Journal-Eintrag wurde hinzugefügt.', - 'device', - $2, - $3::JSONB, - false - ) - RETURNING - id, - user_id::TEXT, - type, - title, - message, - entity_type, - COALESCE(entity_id::TEXT, ''), - data, - visible, - created_at - `, recipientID, + "device.journal", + "Geräte-Journal aktualisiert", + "Ein Geräte-Journal-Eintrag wurde hinzugefügt.", + "device", device.ID, - string(dataJSON), - ).Scan( - ¬ification.ID, - ¬ification.UserID, - ¬ification.Type, - ¬ification.Title, - ¬ification.Message, - ¬ification.EntityType, - ¬ification.EntityID, - &rawData, - ¬ification.Visible, - ¬ification.CreatedAt, - ) - - if err != nil { + dataJSON, + true, + ); err != nil { return err } - - notification.Data = json.RawMessage(rawData) - notificationsBroker.publish(notification) } return rows.Err() diff --git a/backend/operation_assignees.go b/backend/operation_assignees.go new file mode 100644 index 0000000..bd1a264 --- /dev/null +++ b/backend/operation_assignees.go @@ -0,0 +1,101 @@ +// backend\operation_assignees.go + +package main + +import ( + "context" + "net/http" + "strings" +) + +type operationAssigneeUser struct { + ID string `json:"id"` + Username string `json:"username"` + DisplayName string `json:"displayName"` + Email string `json:"email"` + Avatar string `json:"avatar"` + Unit string `json:"unit"` + Group string `json:"group"` +} + +func (s *Server) handleListOperationAssignees(w http.ResponseWriter, r *http.Request) { + unit := strings.TrimSpace(r.URL.Query().Get("unit")) + if unit == "" { + unit = "TEG" + } + + leaders, err := s.listOperationAssigneeUsers(r.Context(), unit, "operation-leader") + if err != nil { + writeError(w, http.StatusInternalServerError, "Einsatzleiter konnten nicht geladen werden") + return + } + + officers, err := s.listOperationAssigneeUsers(r.Context(), unit, "operation-officer") + if err != nil { + writeError(w, http.StatusInternalServerError, "Einsatzbeamte konnten nicht geladen werden") + return + } + + writeJSON(w, http.StatusOK, map[string]any{ + "leaders": leaders, + "officers": officers, + }) +} + +func (s *Server) listOperationAssigneeUsers( + ctx context.Context, + unit string, + group string, +) ([]operationAssigneeUser, error) { + rows, err := s.db.Query( + ctx, + ` + SELECT + id::TEXT, + COALESCE(username, ''), + COALESCE(display_name, ''), + email, + COALESCE(avatar, ''), + COALESCE(unit, ''), + COALESCE(user_group, '') + FROM users + WHERE upper(COALESCE(unit, '')) = upper($1) + AND COALESCE(user_group, '') = $2 + ORDER BY + lower(COALESCE(NULLIF(display_name, ''), NULLIF(username, ''), email)) ASC, + lower(email) ASC + `, + unit, + group, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + users := []operationAssigneeUser{} + + for rows.Next() { + var user operationAssigneeUser + + if err := rows.Scan( + &user.ID, + &user.Username, + &user.DisplayName, + &user.Email, + &user.Avatar, + &user.Unit, + &user.Group, + ); err != nil { + return nil, err + } + + users = append(users, user) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return users, nil +} diff --git a/backend/operations.go b/backend/operations.go index 4e21445..adb4e44 100644 --- a/backend/operations.go +++ b/backend/operations.go @@ -333,6 +333,19 @@ func (s *Server) handleCreateOperation(w http.ResponseWriter, r *http.Request) { // nicht abbrechen, Einsatz wurde bereits erstellt } + if err := s.createOperationCreatedEvent( + r.Context(), + user, + operation, + map[string]any{ + "operationNumber": operation.OperationNumber, + "operationName": operation.OperationName, + "action": "created", + }, + ); err != nil { + // nicht abbrechen, Einsatz wurde bereits erstellt + } + writeJSON(w, http.StatusCreated, map[string]any{ "operation": operation, }) @@ -1051,7 +1064,7 @@ func (s *Server) handleListUnreadOperationJournals(w http.ResponseWriter, r *htt SELECT entity_id::TEXT FROM notifications WHERE user_id = $1 - AND visible = false + AND silent = true AND type = 'operation.journal' AND entity_type = 'operation' AND entity_id IS NOT NULL @@ -1103,7 +1116,7 @@ func (s *Server) handleMarkOperationJournalRead(w http.ResponseWriter, r *http.R UPDATE notifications SET read_at = COALESCE(read_at, now()) WHERE user_id = $1 - AND visible = false + AND silent = true AND type = 'operation.journal' AND entity_type = 'operation' AND entity_id = $2 diff --git a/backend/routes.go b/backend/routes.go index c67a8ae..a6d418c 100644 --- a/backend/routes.go +++ b/backend/routes.go @@ -1,4 +1,4 @@ -// backend/routes.go +// backend\routes.go package main @@ -29,9 +29,12 @@ func (s *Server) Routes() http.Handler { mux.HandleFunc("GET /devices", s.requireAuth(s.handleListDevices)) mux.HandleFunc("POST /devices", s.requireAuth(s.handleCreateDevice)) mux.HandleFunc("PUT /devices/{id}", s.requireAuth(s.handleUpdateDevice)) - mux.HandleFunc("POST /devices/{id}/milestone-toggle", s.requireAuth(s.handleToggleMilestoneDeviceStatus)) + mux.HandleFunc("GET /devices/{id}/history", s.requireAuth(s.handleListDeviceHistory)) mux.HandleFunc("POST /devices/{id}/history", s.requireAuth(s.handleCreateDeviceJournalEntry)) + mux.HandleFunc("PUT /devices/{id}/history/{historyId}", s.requireAuth(s.handleUpdateDeviceJournalEntry)) + + mux.HandleFunc("PATCH /devices/{id}/milestone", s.requireAuth(s.handleUpdateMilestoneHardware)) mux.HandleFunc("GET /devices/firmware-check", s.requireAuth(s.handleGetCameraFirmwareCheckState)) mux.HandleFunc("POST /devices/firmware-check", s.requireAuth(s.handleCheckCameraFirmware)) @@ -42,16 +45,21 @@ func (s *Server) Routes() http.Handler { mux.HandleFunc("GET /operations", s.requireAuth(s.requireRight("operations:read", s.handleListOperations))) mux.HandleFunc("POST /operations", s.requireAuth(s.requireRight("operations:write", s.handleCreateOperation))) + mux.HandleFunc("PUT /operations/{id}", s.requireAuth(s.requireRight("operations:write", s.handleUpdateOperation))) mux.HandleFunc("GET /operations/{id}/history", s.requireAuth(s.handleListOperationHistory)) mux.HandleFunc("POST /operations/{id}/history", s.requireAuth(s.handleCreateOperationJournalEntry)) mux.HandleFunc("PUT /operations/{id}/history/{historyId}", s.requireAuth(s.handleUpdateOperationJournalEntry)) - mux.HandleFunc("GET /operations/journal-unread", s.requireAuth(s.handleListUnreadOperationJournals)) mux.HandleFunc("POST /operations/{id}/journal/read", s.requireAuth(s.handleMarkOperationJournalRead)) + mux.HandleFunc("GET /operations/journal-unread", s.requireAuth(s.handleListUnreadOperationJournals)) + + mux.HandleFunc("GET /operations/assignees", s.requireAuth(s.requireRight("operations:read", s.handleListOperationAssignees))) + mux.HandleFunc("GET /search", s.requireAuth(s.handleGlobalSearch)) mux.HandleFunc("GET /address-search", s.requireAuth(s.handleAddressSearch)) + mux.HandleFunc("GET /address-reverse", s.requireAuth(s.handleAddressReverse)) mux.HandleFunc("GET /notifications", s.requireAuth(s.handleListNotifications)) mux.HandleFunc("POST /notifications/{id}/read", s.requireAuth(s.handleMarkNotificationRead)) @@ -75,6 +83,9 @@ func (s *Server) Routes() http.Handler { mux.HandleFunc("POST /admin/users", s.requireAuth(s.requireRight("users:write", s.handleAdminCreateUser))) mux.HandleFunc("PUT /admin/users/{id}", s.requireAuth(s.requireRight("users:write", s.handleAdminUpdateUser))) + mux.HandleFunc("GET /admin/user-units", s.requireAuth(s.requireRight("users:read", s.handleListUserUnits))) + mux.HandleFunc("POST /admin/user-units", s.requireAuth(s.requireRight("users:write", s.handleCreateUserUnit))) + mux.HandleFunc("GET /admin/teams", s.requireAuth(s.requireRight("teams:read", s.handleAdminListTeams))) mux.HandleFunc("POST /admin/teams", s.requireAuth(s.requireRight("teams:write", s.handleAdminCreateTeam))) mux.HandleFunc("PUT /admin/teams/{id}", s.requireAuth(s.requireRight("teams:write", s.handleAdminUpdateTeam))) diff --git a/backend/setup/main.go b/backend/setup/main.go index d12bfc2..b1c353d 100644 --- a/backend/setup/main.go +++ b/backend/setup/main.go @@ -264,6 +264,18 @@ func createSchema(ctx context.Context, db *pgxpool.Pool) error { ) `, + ` + CREATE TABLE IF NOT EXISTS user_units ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + name TEXT NOT NULL, + normalized_name TEXT NOT NULL UNIQUE, + + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + `, + ` CREATE TABLE IF NOT EXISTS user_teams ( user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -326,6 +338,7 @@ func createSchema(ctx context.Context, db *pgxpool.Pool) error { milestone_hardware_id TEXT NOT NULL DEFAULT '', milestone_display_name TEXT NOT NULL DEFAULT '', firmware_version TEXT NOT NULL DEFAULT '', + milestone_port TEXT NOT NULL DEFAULT '', milestone_enabled BOOLEAN NOT NULL DEFAULT true, milestone_synced_at TIMESTAMPTZ, @@ -380,7 +393,8 @@ func createSchema(ctx context.Context, db *pgxpool.Pool) error { action TEXT NOT NULL, changes JSONB NOT NULL DEFAULT '[]'::JSONB, - created_at TIMESTAMPTZ NOT NULL DEFAULT now() + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ) `, @@ -434,7 +448,7 @@ func createSchema(ctx context.Context, db *pgxpool.Pool) error { type TEXT NOT NULL DEFAULT 'info', title TEXT NOT NULL, message TEXT NOT NULL DEFAULT '', - visible BOOLEAN NOT NULL DEFAULT true, + silent BOOLEAN NOT NULL DEFAULT false, entity_type TEXT NOT NULL DEFAULT '', entity_id UUID, @@ -476,6 +490,7 @@ func createSchema(ctx context.Context, db *pgxpool.Pool) error { in_app_enabled BOOLEAN NOT NULL DEFAULT true, sound_enabled BOOLEAN NOT NULL DEFAULT false, + sound_name TEXT NOT NULL DEFAULT 'default', email_enabled BOOLEAN NOT NULL DEFAULT false, operation_updates BOOLEAN NOT NULL DEFAULT true, @@ -564,11 +579,15 @@ func createSchema(ctx context.Context, db *pgxpool.Pool) error { `CREATE INDEX IF NOT EXISTS idx_user_teams_user_id ON user_teams(user_id)`, `CREATE INDEX IF NOT EXISTS idx_user_teams_team_id ON user_teams(team_id)`, + `CREATE INDEX IF NOT EXISTS idx_user_units_name ON user_units(name)`, + `CREATE INDEX IF NOT EXISTS idx_dashboard_widgets_user_id ON dashboard_widgets(user_id)`, `CREATE INDEX IF NOT EXISTS idx_dashboard_widgets_position ON dashboard_widgets(user_id, y, x)`, `CREATE INDEX IF NOT EXISTS idx_dashboard_settings_user_id ON dashboard_settings(user_id)`, + `ALTER TABLE devices ADD COLUMN IF NOT EXISTS milestone_port TEXT NOT NULL DEFAULT ''`, + `CREATE UNIQUE INDEX IF NOT EXISTS idx_devices_inventory_number_unique ON devices(lower(inventory_number))`, `CREATE UNIQUE INDEX IF NOT EXISTS idx_devices_serial_number_unique ON devices(lower(serial_number)) WHERE serial_number <> ''`, `CREATE UNIQUE INDEX IF NOT EXISTS idx_devices_mac_address_unique ON devices(lower(mac_address)) WHERE mac_address <> ''`, diff --git a/backend/user_events.go b/backend/user_events.go new file mode 100644 index 0000000..33817b8 --- /dev/null +++ b/backend/user_events.go @@ -0,0 +1,33 @@ +// backend\user_events.go + +package main + +import ( + "encoding/json" + "fmt" + "time" +) + +func (s *Server) publishUserProfileEvent(eventType string, user User) { + data, err := json.Marshal(map[string]any{ + "user": user, + }) + if err != nil { + return + } + + now := time.Now() + + notificationsBroker.publishToAll(Notification{ + ID: fmt.Sprintf("%s-%s-%d", eventType, user.ID, now.UnixNano()), + UserID: user.ID, + Type: eventType, + Title: "", + Message: "", + EntityType: "user", + EntityID: user.ID, + Data: json.RawMessage(data), + Silent: true, + CreatedAt: now, + }) +} diff --git a/backend/user_settings.go b/backend/user_settings.go index eee4b28..a757c0d 100644 --- a/backend/user_settings.go +++ b/backend/user_settings.go @@ -114,6 +114,8 @@ func (s *Server) handleUpdateSettingsAccount(w http.ResponseWriter, r *http.Requ return } + s.publishUserProfileEvent("user.updated", user) + writeSettingsJSON(w, http.StatusOK, map[string]any{ "message": "Profil gespeichert", "user": user, diff --git a/backend/user_unit_lookups.go b/backend/user_unit_lookups.go new file mode 100644 index 0000000..f5a8685 --- /dev/null +++ b/backend/user_unit_lookups.go @@ -0,0 +1,21 @@ +// backend\user_unit_lookups.go + +package main + +import "net/http" + +func (s *Server) handleListUserUnits(w http.ResponseWriter, r *http.Request) { + units, err := listDeviceLookupOptions(r.Context(), s.db, "user_units") + if err != nil { + writeError(w, http.StatusInternalServerError, "Einheiten konnten nicht geladen werden") + return + } + + writeJSON(w, http.StatusOK, map[string]any{ + "units": units, + }) +} + +func (s *Server) handleCreateUserUnit(w http.ResponseWriter, r *http.Request) { + s.handleCreateDeviceLookup(w, r, "user_units", "Einheit") +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b390f98..3d53f0b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,6 +22,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@types/geojson": "^7946.0.16", "@types/leaflet": "^1.9.21", "@types/node": "^24.12.3", "@types/react": "^19.2.14", diff --git a/frontend/package.json b/frontend/package.json index dfe403f..7aae7d1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@types/geojson": "^7946.0.16", "@types/leaflet": "^1.9.21", "@types/node": "^24.12.3", "@types/react": "^19.2.14", diff --git a/frontend/public/sounds/notifications/alert.mp3 b/frontend/public/sounds/notifications/alert.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..f4089f283c14a8eb324732e74ce11b6563b203dc GIT binary patch literal 25389 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 @M4375nJgXxH-R&0gWGD5%7veHoJ(^Vhh7Q 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 K|F{3w4*VY&-b@Gp literal 0 HcmV?d00001 diff --git a/frontend/public/sounds/notifications/chime.mp3 b/frontend/public/sounds/notifications/chime.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..c71bf1d69c64ebaee8f46e1890a6139419428dc8 GIT binary patch literal 49581 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 literal 0 HcmV?d00001 diff --git a/frontend/public/sounds/notifications/default.mp3 b/frontend/public/sounds/notifications/default.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..d37f51987e09c388e58189aaad6c86bb00e2cdba GIT binary patch literal 25389 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 XaEpV3+)l%-{U0c5|KI)(zQBJ0oF3Os literal 0 HcmV?d00001 diff --git a/frontend/public/sounds/notifications/soft.mp3 b/frontend/public/sounds/notifications/soft.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..29c89b8ffda2085bb185a2d6bedda32517e131ad GIT binary patch literal 25389 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 literal 0 HcmV?d00001 diff --git a/frontend/public/sounds/notifications/success.mp3 b/frontend/public/sounds/notifications/success.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..3e8b935a30efca44c0a21ba3bb97af8c5ea658ec GIT binary patch literal 25389 zcmeF2Ra6|o_vafJV6ZT_!{F`=P8i(X-Q7KeBzSOl2n2Tst|7QP8Jq+Q1PKxXNg#n) z^84-ooPF5mecJobU3IEXpQ`FlfA6i^-74~eAmE=d>g(w#Jf86Z08QN{&=DraBOt=V z&&T(l*8d)mf7FDSf8Ch7^+S)ohdwTl&`#*mk78V^Hosf{0mXniHSXf$GU0vPS*w)t5(=#wI zIyyZ)y|}o#`svfBy}hHO^YioT>$^K7^4~}v$87gFW=pLstHl0fjJEC zVU45#@PGss1&+|dwG_VGfHpcxu@Lb;BMS-&;`%NlM?e7PGdKSg6!hsLO8%a(lgnIN zTYKa`Js!LGj*g=L#|U*e)X4Bhw~J4o2QObxgC0wK_-_XX;1CcRJ`(d*DJW(ddGzZ#e9+Ob2~CO{adjL>foj_ z=3Pgs6M%~Tqj1A-fi;)UXXXd~>&-5A4XLp!b-nIbswJem*?Zsg94P%9 zc*jgjUNNT5QAqic@BfjJc5N`zdH=NU!Q9>dA02gD_OlENF-wa42ir; zA3V2rD_G^a=kRE@3`Ke)5CPs^NaS12s-X^PBNM~VF!xa8KfCDcm+U2l0w}msXbh0L z1_1JS1JS-=y=Y?t$3s`$`yD1BwBZ43h>c9oE!m%Pcpn>Gooj>`ckNE-}1xypj8 z%#0`Ew()xxQqR#s_`%Kt1CS=tfBZmD&`UKJz5L*S_V^v%88txWA|uf#tv_hb;Ve~A zXiVc}c3d<&O9K4-l=t4D^PP|ZPlD`D5DTLU+th{NSCwH~DA#1=KN8@M_ z*sM zV?Ofdr!SXvZnehhd9MrU=B-zx>R2tVk(b~OV_FeeE3CED6FXbdA~$5%riRdu)lXZj zu9);!UaVL4ryNYVbaBLnLBKMNf^KZ_>u!Car&fLs9`^N z6x5U7a`DZ*BG>Nu0>GZtZR`KzhX92nRcqcYZZ_3q{I<`-!<3X=TB1?VFkJa}V2TKa)q@R$rv5y7n=PFE8TASG@d5fd0`=M$je(=5(1O zbZnXWXX||?UWH&lAFFj=K_2295_^!+ZaWX_`YZnFiz>?u!3&Vg3&e)-$#C#`YtS<{ zNx{!z`+-XAh9f5^myS5PtP?B5@4q6E&S%hiXPk<8ZUAM#$vhmXQoKC_%Bm(MN3!0z-DK>HysHVmkSPc7JC(xUMH2#m?E zy--A0$B$CZ;6oBfQ1FGTch7V@@9ua%jh<`{2#C-05Rv>ot?Lv!ju2dnOnaV@j?gQ3 zL&MR;S1d}1292LJ;NGjdS+1-M6v!TPSQKHwESzhomHOr1X$ezdlJghM(jhI0U6Ml} zexKr=nm_Ev1YPxzov>}b)kSaCa2)+^?rMreJ}9~C?Ng-w@XgWKnBK`Vm=^Jl1=s*z zAb{x8GwDj}D?5AHX!;7Ca+O5QdM1yM`vS97uj^RZxZ}40(l>w3U-f;sYG!3fcYyWC zFOv&41RydjltpR_GXoxV?nIobc>a!Ko+A|-HSF=hB+AV%sEnnOvbGVRkLq;Hfe6Ad zmD6n%gQ<-^Y7;LU*%9APcd|SE-B3FzZn$5}D|if_&)ss$vM(HNBPvt?-eP1VbW~nONjS;}vIO z!Rq=;FXF;D#x7c8XC}(tG;AgBj>I2@C4<=_K3nD@d;}gO)2@ck>PMy-!fn*e=SmI3 z9AdCYGG){hF~HRJ@vsmwbjx=V=lgM{odLdTwia%GDB?c1e_BcHn>V|JgZ*X*)~e=I zG~99@LHYCU)1=wLnDJyT$`V>Fa#(7d78!~>-|7uaH$x^7F}7~pZYOW-11L6VVQ8El zMf!t+m&Gu63DkZ1hFUK314U%X6lLDXj=7=44+r?C1jR8b{l_`JDDpX+mH2reUp7$l z<(cxvQjIijrQFl>9EIFFF9w)c;H#(SV4|~!Ss(W z&b1 zPs-Pxq*%T64a1XlbcKN@rVz}sQf8L#Z&uF?y=;oRaH9ue6Ai5DqQY-PBNk^k_`+dE zpT9i8?$hHc$G;~z)%X8xl4O^7?}{+lk3xt%QLtGAzO(#D>Eq$2=35n5x%oT){@$@y z5)6!akdmMGe{Zxae01}3eSNCc+zz*`wQd$GeBX2x0ALDo1OVt6j42cp5xNPyVNld( z)=Ch2tdbJEM7PT7iZ|xK=L&tKrh{aXyFGu~iHsVo zBdwdj7}Yuz6Ysw6d7bN0jYvpPe!?_LTc!6<2i@pOMPQqw5;Rj)O4($7q^eeP@W@z@ zZ!|ZWJJuzz&h7FKGnLc3kp9VEQ)m~%ypbM`nm4YWq)i&j+cm$WtV( z1H@}%NF?clAL`m$C6C`*2b!Ts5y9*m-G>jo3+cizUkiLCPY4%KEJ3@{YT^w@BpQ??~i5k$Zg~kB#tFEgY2t*B=Z3rK*=as8E7@Ct8p2fxSd4 zSuVhNg#L8_njDsu_;(%2@_+mgN0Pi!pK(iEF12$BR`tVsO4KJIl+03ul2cKBGgh<) zmzo`6DVO}U=4>Fm-&S>1NG;g=&X6OkGUJn>0#n!*V-sB$!HNK7fFC#U2N3Ogld&Y@ zPAi!^zxm=Z2<7^WP#JY2d$8b0Ix*|bSdt+8jlKpOIxeXZa{Tr)_+&-=@Rwe0;0N89 z@O@YaDFdYQl5uki_tyvHgJ9qC#&*3SR(=07fL0s;v>xMSYWW(IJ%AwB6a%72&I|F` zQ3o%899iB(SlpV|L851zI6nux8$nOdae4p#?345juYKI>0|O>6L}a~fm{ej^R&fH; zWa)fM;(BM-Qcq9#AV( z*tk#%#;vk_7)y7|ICj$a@H2`jIKtWqgkrbU<}`jZde^YF(n;hdBv6+;`}XUs6GzBr zG}PEPD*?O>QfKXgKB@5RFAa*AgYSFQ8RR(IrbdaU?Tm4|j3=8lT*W;bkGWS%Q@gR6 zWZ9nh>QFTkvy~o^^qrWlA;z`q-_Sd=Xu|+hVq9;a%&Gdw)Q@CfhA)S&Dr^mYO&yC4 zM@P9m8o>%gi)~IYC_mfyT&I}36ggFv*(x;mSEGN~l!J#*(Samgq0ePc75%8Nm0La7 ziROXRIwE#-PT%cMDDsZHd~7ZWi98?(Lv@GvKQ1&jC>15OiH#6rIgu@r7)Bk&5sOVu zkIrW9Bnu}y9`4EbXqI6W^S}L7QouFZ?2?8--s5mfsY{Hp0$|4 z-q7XVj78g5Z?R3KeQEJbv^L zF|<;&fO>BM@?mR8N*RilFl2y%^S+{y4`4brnL~XRDS90>-Bl5bt;xTm_@sLCryZkz z?pJo#jXusa*1ad|E|+KB3a}ck4-F@IGeLMp!??CrGh|*AAGxwu#sbr`DJmdzE!i2g zLF~B+dDOua2qGRHFBjOLh@P#raDhRa|3GEfG%%{u!l4bl%N$uhtjPDa{i2*}6=iV#_@P$nK#0d~5@#ZJ5x(oHDxz1O?F&^)qI2`zf!a z8QRXylc7!xUyj0hSL445+-hIma=+>v_&nZ-V8-58AwK@t5~B7;Oai^>2OiV{)U0Y$pmlI7ukQ{%O;lNbA%O) z)~jO}#XF``J)^HU>`+3Eg@li?6u40QgDT7Y0c>2WfcSRB^;21Qz zTi))zbj=beuq6HOyQKrYgvtHCkt?p)4zKBuqtIQjy1*2sy%;#kw&0i51IW>ly}1z@ zJtRQU*YzhObj3E#7=kKFg+xz&e+rdRb}Wv#v8*Q^7{xQ|8bM3M_g1fmW`fu6Oa~0QyoZq7Z zg$U+>zt5KOV0n_5WW#7~nkbHvs7o+$BOys^HOw``{U7BT8RJ&VAxD+g4D=Uaa!t#L z+6>VFx?T-gcAk9W;43}Jv|$GTgOa@JV}LH1`V=^WI38%(M?~n#f|xN~Z;(&idm`&WV97l?$ z7*R^ylVp4K%54(0MS2GJea=|wA8#c}#v7}y=98M1cuKyx;jl_(n$C$>ca!5Vs@9S9 zTlzn`N*)zIr2TEPozG6Pr73bcnFO#IDG1BnSELNFc(~o(BUeJk&!_VzV!@vXk17St zm!H;JetnDJP!$x;gs-_h&(fx5nBQU6ZZu1DN?o><5Dr9-kWl%EY z!I7j&Zd9e-zmVUrWwdg|`2n5pYSbUnjZ7$H+=tL?tW%#7W8gPj6${g2N-zD*veaH< zZ~Hn|3EJUO4tA8YLT=YC6YAIu1gbEE*6+BYqC;k|ep^P_e+Xys2w*xd$Ehag?k;t@ zSIX4DZu-ic-auBj{fFxnxEagiQIU6-*#TTeEbm7zww{}RN{B;Q{lXTpGjWvo*YYDc zRsp!YQl789?LvR|-}ncGgiv$pG`H;1m5oBf4^Ny}Op3ixP-w8f(!PDX=Jr6{r(cF; z%KUvA&BiMR+=k^3A6v$GKdI7na!wiiR`EPon|VN*-5UiJ(D$L%>Z*Pc6RtZDT-X?k zIL`JwDx^qvS<*QXhJUctXPFAqHU_hh;%32EGtJHJ8Ui(@lx6@~sJOm@GpVkEGJihv zkLk@@vou}xw<_yN0BU>`-{UOco*Q-*d69UB1XQtFtR?{P*(bKtgpn@N()G_f81xTO zTN@D(!Lxkx=68QWpMJ$wY3^)s97+94_C1OYXAIQc-Q!$>XlM7B`E<7^`!q#Qz#`B~ z!OiNg2H;=p7W#7#5)1+q>0@P{P+9Wu6~kw56#STf^zpU4o=oV}!Wu7e6(aPPCGEeTYF2I??o z4Ft6#e;)2oBM&Pm-_1{naGi!#*pj4+nKG=6my7;FhR$E?|G86k6}74MTQX#DQ#fd1 zRuc_<=tr77Tz>ue=Z|h=I!?wj0F64D2@w1_ofbHw?8pzS4e{$$#3qVk`6~cNdIHOy$G#= zmzwhei5iOHt%Tnlc-kE0ykpUOiH?lD&&1t>J&#{yA1cc25OH8Qz(uk6G?NMISVC?p zvx9OHC(&Zbb2-Q%y3jEGx4Lk+Po~ls6%GoI<2(r(P)UNC1BxY|uOvyi(XJEnml5VQ z#8~!FF$n!Nj=aCZ7D+wN=q4 zE!tQX_>-M1@orV5$Gglu&Em_k^KH#hsS|82$n^5ZZf?fu7|krYwIPxp(UN(EnuvCTi!EWkaCKtFe)0|`E|1l08upps=vfVm zpv;H-JBqMYl}M8ahrM7ZSU?hEt|KO#kBt^fVkQteLlZT_h#iAM8dbzN80VJd%+z#1 z`$FJdS0&xkfH;F@eI-q^UrrX8rtosbjJGqY3aRI@=1#A*-#aQM5wE1>YnJ`&Xr~DN za>|tw))u}Yz}UXvg=|?+A0ex0u4=Hlr*VwIr-Ng|_!j}xrg$`w5*ak`4j-vbf}aWq z7l)S2fiwCbzEpF~+^6Wj{D9tSPVGCs4f8*~#q~*YCR&tY^b{yU*{do$n-Mztc!kHcx?}-x#Edcm(TwBFK$)Zh<2kKpYW3q{>~|N@TUtp~qUC!Byo@ z><-`>ufc)16&X;go*)>vF`3UHaTORX@63T-3dN$4o4jVs+x52qXMGK-k}dV1d+D1H zvT2`~H+|CooRPYm4l_89t)x7Mk%pDLpw+WOt>P|4tQ!i-gPUX$%19%x6`>fn>F*l) z->I;gGG7@cl)ifIY7c+|_%T&cXwjgMf+ubje8QGCP(sz(V*!k2%gq{sqzO~fVl0K< zuVJE9=j$b3=7`Kzp4o|DSkW!YD*y+0QR9{zWu_66_u6m>++5s8{(@d?a7Wwk3m;AB4TUhFjuLZE^u*D9 z3aI;A{gQ0)gh;1!UwxQ!OsGtY-SrK)VHn1?#Smp@u$Ru66bP5^Qq3nLZ47{YIJ{RT zrxt?Z0Ud-WdQY7vd6B0ki_!od)_QL!@}^RI9Zf_M1eOXO0Rse)K{E6JdzN#4w^+1p zj4WJkfFDD2e6tt!pZymB0IJrUa&sx<^L7cw_3=p5HQ;A%Xh|BZDz=A>w*RIo7fMfv z?>V*QA7A0`*xT^|G)ho~2;vnP>M-g~=&*7*d|oyj-!NdsBd}vd?84j$1X4S*5-{Y~ zg(FW)9j6`2y)4VYR}4C&)|N};Jhs8}t93{n5@N8Gw&=H=ZY3GXba@|_hgRbGVuuMa za71R>H~{8ObP1EYTd^60VtfB!5F)T~TOpeyK7w+IR25P~;Dt6Fw-8 zD^D(Sp2)S~!znC!s2Io&$Cd#AS_5}e7Hp{N3M&1AqRI>^0jQ}2j#5nCE1#K-%2X0) zjzH2JeF75}Ml0LJ6SxKym9vcoLG&tgo-Pqf=10#-+e*#d!jw9;Z<&*_1=$G3WB5jvZ`_EAG9l zAg@-V%69#FfM0tvn;Jl60Av9G@=bL14IwH9cVfK?~^6L zEKusy^}N|sd=@qbZ1;?3bNSuZBF58|XC9|jA2xn(v{3#m9;~`u_0VaFF3zacijE!S z!g5N2oz;C6X>|Ri<(5*=o$*9R{wc~eGpkOP7jmWadunl*1?1U8xcZecQ|Mhg^9(ND zkurZQ6sSBS2#_VSRs_mYzoMi8D6hZ+ldaHss(RcEuEorfpYw{_IQhZItA_62P{t|{ zk3m8|6{JHk-p2bSj;{UME-&QDL7C;PMSFSv-CC`J@~$Et-%fYZ2@P9X((mfQlI2vUl!=~K8+vZJm= zv8#GoYM12Sg1PG2A8Tr}Ih3NFCPlxDM75tVnSjm+*y1dc$QaS4I_O*nlNU$a~Hw+FI@0E;>b{SnP*?GgQ zh5ThB84MMSr0-#dcTVXo!KYsY@Kr+{{DA>Df1QXDw>4CK(h;+bR5$G-!nT ztnT0N4}jA$8CVxD?p<%g?e}>K*nYUlzzxdlE>Rg1Zre#ge7}d7E&=G;scx_eoVvvt z931nMWAC?vb^RgcjfW~(JvWQ2dtjcqTNBK3eN&Ai4r(?k!zlz`$d^(Z zY3@<~57HNxlGMPB;h%n?CTVr$Xauv1-nvbg^8Nu z@$aT%m?U73htC8+B%i|kJRUA4V=Zlx7mljGMjyhwXLbs=u$QfhAnCeHc!u9Nig?hycGqz5NLKf|8kH)|Q zd=yZX_D@R4Ke=pOsJa-pU;;&_6p4zvm3*>AL2eRr$rxl43V|~!Gj5YGxl?;u@i-iJ zw@>~EV~IWuvta5UKiZMRJnG8(UImw))oi&wX|Et2{LmzpG?_+v29jZ&u3*fI{DYzb z10RDv-%X;j2fuaEXuiYr3Rpl+SoT4bObU)8a+>JQex*vh$5aXjH99yo80T+2e`eddCVVz~4GStVl2E8)(3xW0WK1Ut$?x!xMTn~CNMMj0m zi71<&ssRY>5bg^wX%bUoui?s8PaDD^VeSUcwzAXv18fk#lLW|H;P0!~{BoPuHw|ms zhUQ9#XB1aj8sbPvn4({}n1A;ndAluM)I&>Zo?WH3zB;Fmwd5_6!|ABvSku-tZc*r} zTq(#vgp;aRlF!YauIY|^XPP)QtdFfMjCRXDAcYlw{4=)HY4Vpz*3$Yqrd=$ zK!7)2AGWw8ruotkB$}ycI_SeV%ZkkksH-h6<35=CENKt#tnaxe6VrRNG_#k3A=RJS z@;S!IE7Dv@vzM`%E}bL)@#7wL?WL~t$j_CpGMjb4`5|FsFVk@=|6RM?Q$ zYQCp{*!XHGt}w&3(=#|&i#V3PYpopRQzaT~CSoFFNDdDG&~)XQ!6@rtX*70GI#m2{ zQV4O&52aim|3&5j|3ka9YR+#>k|`g0rYg}9@0J2$?}Q|#Yw|8od&kmvh{}_z({2t% zt%~E|;YnZE13$0ivDADVV`)|HX@G7Ic|QAz_+6JzJelT$hLSK8@Z6=g-{EPwCVrSJ zIi51VvkAP%Erj=5m61taT(b=?68V#p&G?CyceSXsnz}uQ-7$GJ!@y=~3D- z!J1HBu;NJDCCBpv+hW*hHTVt}Ti0II+|gv2mOX(>8AM2G-IS(NnMTQ7n#I7@lCD5z zdz|gPh1k?(#n8@w5|xM-8%bsuf&UzNQ=JHgqx_)D5XD}+Kmb;U5FIDy9#9#Jj4Oer zO*Wh%s3fZQC^?zXXt@5)^mt%d|JbNrP;^m`hn*%MX!UdD%(KOdytBN#BI&13g0L@i z?;PYUBH^ZMEuIhBNgTM2Y!;S>mRv(Hu^p*=g~l1~}UXQYgjz(R852E@2p%s$*)mnn{b zN4TL5=INf{hf>d@%Oo-Q{vSUvAio;&z-hr`T?EoK70xW*Iq^DkkBgMNi!wS!2 zU&^@h2O@8p1MFgPW%c_cj+~H42&FsLzaDGfwv+YTL1)i9_``cScGdpVa@f~RQxR{c&LS^OzKz%v8B)IPi_3T9TJlmOIN`eU`5?eQv^nR0CG;!}M9;s@5=1qlx3yee^6_25J zo4|-OEA6LDJy1>SKBVMo6B8qX>V%xN@{+>+;u!PtGB6yLN>ex+6APSl&&w&!c9=qQCyH*wcD+bCPT#b`<+cf_#9 z!8XuC)A2ff;Yb@`W+sR{bEmCZSA|V4{_28M&OBynT+hSh%QdkfxX(bk+&OrS$W^_T z8#cszd@+Fl1E5?WIYH9?6!-75(NYi{3j~_)F~yd^ulDo%XZ`GNiFdzyuU^)jd)Dn8 zTckPt{>bKlF}Od9r40|j)bEw=41F>0H<$)Bl#F4^OJSfv7uZIwr0rQltSe#2hMeor z>t!&=-bNXWX5FRkPOk0{#)2z^g3DQGtzU<6Tsy|Mgg}ib-o1%~zPceM4O7!BmF5_A zyV;MP%KMD%s1ip~s!ve|TdBV~w#UALJ%4^fOV*Lsc=3M7PrSEIBiV}l+}I7L|0k3r znVH2QJLFL6u)i01*jxW6QC9gwRB-XC+YaW><~XzuL;xU)J{Qjt7eQHRq?xOm?H)!b z&^q{NXUuBc}Jw&Gpzx(2o}X*PF#q@MR8(NpPMTQo>u;)=wRvR6;EG7%A+?=nlWi?>TK1Ap;vW!<(OHJ)Gg#8{Lwlx{NOdkW5%+#Y z<%?`jz9$o9;?+Q3w=Y|rDv#uYNQ%6!9=~55r){q1o0 zEdQM_`QQlS5RbpP-MYAZo)B@olNQ_P0(=tD!y)6s@?}AosrsdI@v?YGKvh6GI1Ws{ z$YLN#NYWd^S3>n6g|ixf$PCz^Fs%OL=R*XDQD&6WYvj@c^S7h7hb6=VjSNpfV=$L) zFQBEv+q}BhgkP_fLx~xaD9Z_d=#`Sg%AY$#V*-wz50*=O|2nY_PXt5Vo3EA~EbGi{Z=Xi&UidUjtrfi4&)t=56jTa4 zqN6Y}{8XTH)}|O$Y(k0a=~9(4DA5vso!QN`udSrI+RAxA(S>5#$mF47^0M;{@;C)q z*pwNy6Ig4*H{biYA9*KFZ~LYCUQ^DEcqA6iI#N2wUZZ`@*7o0yFn08F_L-;@mp(4BJ6GFWN|{iWj{rljz8 zQ>u_@recFYmQ^0=3-~feDyq7>mSard+O(F8PYokxP z$@3zD4Bax9J@DJ(Kklh%Pr>Ov958k%&9yA3g(O10GDi2~{3QBi-mQ3st2hE96F91IAJN zI&%QGGUd`@qwg<$0MLiRz4o%Q2UQg$@&(}WwmVF{F9{BbBzSVb1Jc6eZJGVtZ)=z` z=TlZJbw{#38mByvc;h&nC z;Oz{RAUL|#cW&C6yVa`e+YXu^4xPQY%xlV+${J1jYQ4!ZZHR&j+kP`4g~o7npY_rP z$5E!tH`f8CN;BDwdZQ$Yk8Kh6ZS>f$+4G!4cGcbAEH3+VauPj__{vKbN87by`KE=V zWLEVvW4XAJtwemHak!_EZv9?F+544L4VYn_>*tL8d~$pUzf$)mt69TY<21rH>=n+$ zcix76{?v9mK5xhP7nkcYMZthlrYA53cK|?%bl; z`;-pJ#Hz+KFM=acI?$;%An16S#?t8Kq210ogPFD)oW2l4q1FVB(c+JP^lG>Q$WvzzshfN(f#n)I; zvo7*P$-_#B)uY>MR}6c zN-%`Mvy;`sg_X822l>!5vX=1XFZwU97e9T==TKf7c!5%` zDH@%LM4G}{+_Y3P)+KsAxwzKU_AH%BKuE(;Q1YVWhr^W1>82T;Nl9X?;DaJ}DJ1q%M=pr@r|-UwKs%m@ z-iI0YfH!s*&mA154xC!VM>MTomem*1vWBVQ&!p!v?Clwt7VzaB7X?(BoA3TwpI=*O z(=?5K*MELeT|aRIE0;o<3C|n(U>J~i9)4?A@45-6Fi#0`EG+$;BUOJ1 zXO6>|5;jM3+66K)bt~n>+vPYlnC0!sK}`;8s}Q3c>2~bPa$<>Qw&#L|voG9ilEwBN z8~U7F)%ZPC9fYo}xVn=T23`eoFw~NAmIzz=7TOL#InYn94t$mmaaBM=U4a+xeHmYP z%X@ zsf#acGD9zIu_gTZm=naLmQ?iqh0oYh?F&_UQ8~w7_1jxl+|w#T1^AgqR*IMedFm=a z8!vf!M5~e-^N^mbtA{C6wfTnNWiIG-0yFcML;Jlt%_;j5Jy}6h z<{*Krv;%PYvEY;1_kSUJVCT-< zU}xe+p564FJU^6PRYS^#d++KoDt%9Tx2@UFAYck%Yrn-?qCc zuN>~cOT~3BT}2J@=PY>)b`QE+%69l9JT;h&$m=g;nV+|MTd8g@V zl(MMNAw2RN3X|F_y%`z=g4s-HOw~Lv3WOcGnszjs9+Y1O9**mxbAQ07m)qV#WtAWN zp`b4G;S;#Jv$Y`LC1iiuO*39A7D#-#Lzu`>juUAa+cstGq^NlzP8~Z}{_NJ3ucVYUg)$DO2It-1#qZgsk<+wGU4IDW6 z5hIGImC>C|9Lcki;IFN@8n_YYZu9J5R{W6)MVzUq;S32MmDekkHzS37Hj(5MNqUEz z=~tBdi29;zV-Hiu90m^|zW#V}SYpxXFC|Xu>^G-B${mE{xo$XHG{_9p4__AkDp<8s zl)^H@VqX2I`D1Rcu+Wr!d#OUfniWd4V(xrg;A=XM-l zpmtN`FF#8YSQO68P=1bGeXS*&^co~AU`{~KA*`uv{$_7v%!uG0Ki_epiaq7fR7h;2 zIG#Z~n2WUSk@$FgkLeJm;`Q=)Q5ue9xmZk7yzzkddQ#BvO7GIG>XX^LiNiouCnZiH zvxfKEFgB@ob1kK9X$@LE4rpaVQd47zwSLTD=XnAX;USsrpoZDq#2${@H6If}RHfg( z!qa<+H{uP`bh*3qB^J}gB|bLF1+v|?BKYi8WNf@NAQr5rcxK!=V5R_hCb@Ov%Rp^c zF#|jf=4e@23gzUiG?En@SsxCokE4o;7d0Z+M)%JfI2@iJoXaX5$#9*WC^+^u22Hac z=2`Z-oXU|u)^8TSng7fRjs42lUOLch71v>Q%u&MdkkSiageZbpj~f6*rJns4^(Y}|A%Ot28&nlSZ(cnGP6=t zaSuwa#_=poiYgw)cKTGGrdV7y`ZF&GY>4H#z30lB=FN*!H?3gApC8a)!43A) zo#%dUMf!do>z#b`L&8A&`<|9fV+=%6Kl*FP^GX3m$OtBLtl6-|YJOHM;7C}4r?3`* z6RB6q{VgMfj4Da#vevS4Uyn(W`kP<7e$+4A*fK@Rr#m^+s+k&zT(bZ8`3W9nlON(# zr?GWWWMlAPu_U7Y4sMX*oV;nE3-6$JDaC-oobX?9aDM(RBS*VSGnFPz9OA7Bg?$%# z+OxZNoAKh$F~^?=28JE?_RvMk{z=uFt7r_BHwu)Xq8cb3hFfT^jUu|S8>jnONhg_2 z{Zqc7!|b0lFA*NEpU%ws4|mUxCF!9V)QmVMB~ysejuQpceU?1t13tbvZ^2+Ys2vjMI)RsXrf0fxwLVlUC0sJFXZTysB$YB||17{;w02Q5D={_S6C zKk>2+8l!%Vbm=cB3!UA<(F=_Ff(r@*PuI#cfC;KJj7nzC4Ic3$ZeI}Jh&Hnl1WNj*MIgf~>hGBRhrYJrK{P2xFV3Z6Z6!1KUNTO8%WVB9AN_3LM8Wa}#6D>u5{n1t4M#=wH;{@w zF33hc15;vQ)`X8S&GYj|dsQ6V0j(QcPhYSv?X&bgi)K~B=*|s{uwV!NRP&@@0$ZmP zEX*;!_ciU8rcIi zVE=rDm#W8EbejR;Ui_hopi|&c*Aigq=jQ)g^z%wSeeamEANxRLU(TI*&HAKcQz6^4 zHt$kKc{jP*qNxiQac(f?V^)mKAE@a1kN%`Y zEh-O2s>5VosEvPj9_}Hm!3W{-1*jab?HH6Ni7hs3F~H^VjI&anibOU=xr!{|eCcLC z%@$XamCj)xs_CW)vS^FJhpX5+g9Zt|HN~0Hy!%v@jGVK&kQ7xyO8lf%qX$E(QNkig z{Bhl%VRzh>-f?nb3-=N-C*T&p)}paNFoF+g?Yqsh9C>06mc2E}(a|U=8&eOpzg^lZ zOZnLOKHVDUJnPTwGa#a4@^d%Px91p*ptN<4H^%;4p~YWkV!9mSUT07`Qui5~6HKhB zB@<^;jwe;1me}tyh+P-#zM`fxy^grazj=MZ&c%Bsa)9V%HU9pk`avYe6_G7;Lz?)0 zjOuccezkPitk*QVuPmk(Ur_mJc3M;ce&l4&6IZWh$G^H0p7fL^Ifxtyi!`1D@bu~} zV)2=#@9>=2>M=eM(uXIDXWa)Dfqmy_sUKZ)y^4E(tXCcSL~^ofE|&ZG@rPVq4yYO! zqb!Y=xR`fRsRhl~%$|9WImwMcXN;rgg+df`rEXn=+owTuXfr`1DuZ}^sV?mSKMMP- zDXT;&Rv?#v7s0=fo0XU`r6E%K-BF3HRgOp0;`|_4HqyguOGPlkmgUn}l4WsEFK@u! z*-gmUZ=IC2t^#)*k%5gyj(Fh-*Nh>bh7>MP(#?LBr&qmI#FVI%0@z!MjVnBxFO6Q>28OkcaFXu7|nx4F@twB!Ec z=L|3Nr>S(LmymB@BOBm6Y)Pmg4QNO%PL2vfO)`_bJq-h4yyAYmUnKkT38gcm4lXX# z9HTnOr8DzgAQf-cRS8VrvKn>@+g(f26W9Emr_L+8I;QSl{RtYXT zB%w6+BsZW|Ghwu^6QF%&a#bxihQ;K8A(OempOZRH=$Lq8BjCxTQi&%|d&=4zvc66t z{+QA+M<>!Fl55ud`s!SrVv^9M0p>j$_(Jrl&t+*osea-HX@<~7{EVujm;SY3IEYkU zzOvCy2+!~kQ-ncgI?hrg^2yTtL2X~hcBsjs{ z2`&jva7mztd^7V0=6dSlTXncQ7sv= zV;p=rOso7~;ygGV7p-{A%66noq2+qL1EEcH^n$893dB~LM><|?+B``~&^O&&f4ybF zC$8YpHAkm*c8hOYky?>&83zuL-IgUBKU0>ZkxV8!*FK4X|7=R`y-)oG&r?^~n;@EN z^Z5+Z+CdJ4u*_CP==Mk;_e7=?jd30k3mlcQwS5vAKUG%b>_7U@Rs2bq>WMe4f`|*H zDSRpM)IP9PJAqcfM9?XfZu5Gn6za6}AoR ztQogx;}A+<`_caJYUS&$;}Zxd_>fc^O=S@uy643kd@xhfT>n|IZ)5DeF}HtQTS`(V zn~>rJw_Pu*+ttij`?nQARUQ7?0yK$?V59oI%3s{1tg(hhX1yG}L3vz6`z#&K7--(= zk$r@D;nZ0=UH~v?%KRlhD2Q|+wX7`HZTx)%b@50;?pWnjeC=|$f54D~u)@>&}dr1lCWQT9RW|_m%?z>D!;H(jQWoCBON$k2yCj zOt=pn{jAkuA7#4Ry>}kG@+=9@x7CyzB=_jenO$&;GRjo3rIOVvt_YbZt`0!=y8tZ( z_;8g_)xK%KAq5k#9Mc~x$hGdbD)S1zuh|xdwkDt3(WItyyBqqv2#}RK~(|%*kh0bSI;h7m{ ziv0a#Yyl;oQZnB0yX4a`&%8z(Wb1~JM3@+X*<5F@?o)c}#iGY_?LxwnEdS^4tJz(h zTvh-%6h_P@E$vmXJc zh{JKeG%Bm{k)12C(^Z5dqArzH*0f{^=e2iK7p=t|1C!E3@eNp;+toiZdVfQ2J~Yxm z{MkT4+|vFKP+xm@lR8ne`#QW7C(($O*r{)wv>EdVnyz~DmTSkuNI*MU=9JJL+;7UwFFaS87!-_H1O@FVfld#S5dW;|5oCXF4|3A#B&f1!a9{34G;v(d3{LbQFGRv*qu6$P#mF!&zDu>}@QeroC~!I)nyG=iNJn-pk{RSAR1 zI|j%a0Q47SGl;LAvI-kIQQaanm~3=}i_iY?bBm5V=OkJMepl$g_8MT?&-W_x!=tkB z65uJAYL6rXEmmqODcrffjs6oN@6zEsn;!z*tcQjVPfmv?hDq&T{k6PQ-5T7HVUjE6c>nvBcGAPq*d%hB7^P|Wu3!i^4d_pSqLe@EM3c#xH{RtN zgE7Q2by%R-+&*q&7Le;u;*hRqn3RwydSei`LBDCFyKp)$Z*5vtRpfv_?%8B8SX%gs z4uA*N_)i})dkT(*Hs3b6su&Q?Qi#*JOx~1dmQgKup&#G& z-dt%vatzueAMjHY>r8sY53z-gg&1Z(Pd%d&AcwOH-TR?L_)S<}RhxHJpW`A}U?v`a zi3;m5_3ePY0Out6y;gm3nxQV~&f=J}9c{I0i_Y9gr>fCWl?$C;R^K_h-Tb4o#hQ3Y zqGA&Kq9e2D=%oE%!obTsHhEVu-+hcrUd6bAmMgHy8dg`8{|I#YLaYl!xltd*d&NQfO1#AQKkKhMbQBktE^e@DlI~bJnlO#rtNorw zR^ho3&8jWkE36>dk(+9LEf&+)odOXI5eA!A8{ix8lxA|&9<<>ki-`7SENWlpp{yuv zGs&e)M`G4e|KY)F=Hy<7Ueq&;JqB!~IMIcRsO5Z|nv#NN4e|UvpWtYKb>k^=j;R!{ zdMbNS5sTNsp8FG;%M>OkV7SpHb-*;4BSTMM;O3g?ihaxjeEVI6s>_0QMrT)qDow5E zdM^ToBxG%nu#EpYJPb22d`eXvB?I%;#KZ9^K*RA;WLHsks;P#=D{A~Q{oRyjSFeHI zpHz*n`jnWh5ycyyJkdAlM3nnY30pgUdEw5=~|>LLQ$@f{%=XpM*~aX~j42l1gzjUoEvTEL7pEW{A0Dtaf%V zEy`>S3*W5nR5YEoQ&#d0PG}^ipv^EoBr1A<%Z`}8t>(RrYC$RCqsnS(}9W!UH& zUx7a!W~oHkQ;E!bNoVIDxhR)9$6|mh$RX~Z?jZl=O*B)E z{%FL+u@4_8H_>#!8B{x~ETC+5?M8^t2gh-8JEid@m=@y4JVg3cjOI`zJvWs^J@XacQO9!BckGI)QB-bxLh82W-lJQ1pJ%(YXL~ivZJQ%qdfbsZ-oLUU@)t zgVVBtnJRO?@MaC*%XBT$EK+8E@vSp4{FAszaQIEfoYC)kc%p`Vlf_O4!Y9zQrpV&Y z>>I{w7p~PjGQN;3@L+6zuHh`rvFuoX2&B;_=l<=NXLU*4f?L^K;|p_>NY(iTOvg?vBC+Ib$c_2DUz5kBl_=V{5#uk;02Pf#b#g=mT39p2qo;s-IfrU1l8Pk} zSP-P8>9)lC?B$VWjs< zKoTi(#;?{fa>X7^@O8_qE?#v4U<ce;m7l_|7CCMTek}hHm?Uf{uI%K`cP>+x%^2kXip?(aF~iG_mCyP z9o{X+^8I-rGkC!}QdJNcAyQHi3hJ)zI|-BPB#8bul{M zN^F$ zY|rupGAR1W__b`p;SUv(O=v4txHKd3@PO)5zR~^Lq+y?mua!7tFDV-`EVT_6;}Pko zEHq-jSVu!A%4pd4ZICTh3|3MU2!QVsxID78S+t2p&1${LEq_0%H=^}tLs@M-Af~?I zgTGh8l~A;N{mWbhOiGks*o>2@prGIpwUe!Vm&X_h|C7=kjd?*4i*VcA$muzvDMPwH zTz!#ig&N1uiPdtBqFW=;*sE5&&vt)!sG9?NGc#9a8R9uDZLR9t$Rg4$r7exMI12G` z2S`7~H?W4AKhfxCzQro^Ez(exL45bQi!A|NPHB`ogTOIE>#kN!E7g zC-N>LsYK`V__x>*N*WXKWUNN^&GGRK*K9U()ag+Ue^5SFI_);;>^I&1s0qq5&bER# z*u$GSavJ1|=_;hzBxUgQPXRL>M0IFIcB}rb!wYg5jrUL=OOl1Bx8ZW1;Pro=6aaEa zNY*l4?b`#YJk=ISty;Z4Sq3mdSlH4+{LpI59qI4nGrR9}w?5N3WwhRg8|~E)oY-m> zV-OW46|F@Yz%fP@?_0Tq~|tSeFhjU-WUluqe4kf-e`kJ&(;o;X`55a9(11`U^) zGYmGqspYxnl`FL({iTsoWSE(n=u;t2VKgnNwFgv#Npb3hP4F03`4;a*oI`~o@e4vj ze#RG9xtKfZbawl+p&LI{Kedy^0FVL^GU$T^XpKTagzlKq>ksOm6D^{cL?r+V=A(1p z`g1PKz_UrH70!Wbv+v{Oj0$>PnxbtjWl4M_?kMG>3imm_R`dZgG7K}u8RYuQ*{91( zEHrEXWIb@czD(>E;kGg!(5L0eK6*~lF!dnBs}f^3nt5ecTi*HN53Hv%Zv6u3}JA9UhmY=XvVUCUchTf^N$dkXpmvHc1co}U}BLq2j?sn}-ryW{ek=Oi`rydqug&jy? zIT|gsC}~UQqcX=Px_*^O{_j|`z8+oeu!KZLA#<|HABFlq{1aksCULP5+MdCY=~UY0 z%9pbw6?tl77%p`WM1tCpTpFy7A^Z+&qkmUrM@fbi>X)0wzlF{1aZm32mI>{>`TV+; zb}-?|kYn-Udr*eix7Vi3mW3GBNv@nU$DE8!Zi%z`{lD`i&C>5}?MM(Xef`p+-o>ka zERN9$38(eCc5g>deE*TUwo-uw$4>q&iS6wG-|neYgmRxJg70)rCv%J@bqLK?C^NB( zUF;h(X`FD|c+he2v?w*H%b;B-u5E)>y4i{M^aRnd6sTSB7nzL=hh5RWJ)`Aq-xP^t zQT0gkD&*7*=dJ6y;!LGn{)}w)O?8Kc5(N^JIYYuSkbtp(G;r6*nNz?@BKy{29BWX9 z_hSOw1LQVO>%;)oh=4Id89jm@02Mt}k}P$#VK*?Rk*0SXcCfqKj$7t}#L^TUYc zH2JoTQ(+_Z_~N_Y>l$QTS*>P=1+AQ0NLIQUS86r=Dt$TPn`b`K!74I?r~BG1r3fJx z!$alzkEjt?rJ8T6)iIUQ2%_rfC!Oaa06__zMUc`qs+Z_(`i^}Q$!rU9dA$j)j(mMl!5X!`$Za+bu0XNE{sdQ~_owc+1S%qAJ znqQ_irwukT6qc^&j-4Gcw^d^c{bMKG@;+?h=!eD*7uq=^8M&n6L8t%t*+PRPx^#!I zOZ}F%$Y2D~sE|PVo+n=B&jQSwJw35hqb8T55Zvuw$=YoX(DP2Yoa|?!xJfXVAcnEu z<5zywHjT^a;|lh%nm?a!C3HUs1&@$O4hWZQol|6X0tqM$4zMt;&4Ss`xvhdnBC+uH z1wy04Y0dvAvPELMz@EcV@DR)s02wJ$b4z-vVrR+98BM_;G2EoPGIO+9kZ47*5bE|*g+iMYTull#;{h<>XI=_Q3FEdVLR z6cJ@B>YUYXa~JeGz;eb5e)l&m^?=F(!7pmdP5VI=4*s^rBuQs+JYCTiNda3dy2D;O zl1@%@j7U7JiP=^_U1;KGvQ%wuxf>#ieWj*(1+K>k28mA*>v4=R6Co*q2_z9@<$*c2 zkQ1+QQw3&dV2CC*w#JOq*oX30Z{oyM%F@*#Up?bo6?vtwrHeILd5d%=TM*TW!qb0H zusY$7UlDt}&nJjsBASfisi4C;ppn;g$+jiBS|G4$NqvlxPIQK~jRjr308voZ$b*NaKp2L4JK#o08`KvYZRO=I%0ekiVpW4rNU&U4Ki{lY|!1MWG z?}j(pA{JcHxBcu8D%lUSxa&A}BX9nKB2o9`HGI~%Z&*?60R1wPpLmZfSR_?@x8;@ z2r$o-r(+G8Gj&RYD(L^!I*IPSGZiLlp;NqIu;53HFHBVkVkx`arCB9jAU1r9=OQw+ zgr!qtU~6C~I`1N{q4G@2Zdf`v46RV>I&YtS;L=}O=j&akJnOeP2u<$8EH=4)t8&kY zMuAYiVZt5_lYds5+EK^8l}P}`;!QSeb(cMaods`t=$BC1!pSQUWIhD;mODXpzc zq^7xTl&XR|3Iq|+#TEB1W0YG*sTO@njoQUHWb<|S3R!J!e7Ut}Nj&^=l{YP+ss z0Ok!w9@<>*p^APu&yvY4shk+f@nNiXI?hcw2c=Q&9V<^&@g2mDEof+|-2UTdFaD<3 zP$C3O$tCX)@mmey$fK|UXB2h;fn2+1TUKC;bRg|dkf`?2TgHW3Bi(M&2HXP0~ zq^*q84cGQd=x9~0JD{RU>H8Lv*#A|Pc%RbG)q|-5i5uths&VD3ehJT4r?Mn(H2dy2 ziuR&@+N0&Xsl=xRNSscuXq^Mzei@KSED`P@k=wHG2RXh!CefI4ULY$u9x7x9QUZ2ia^zdCG*a$4JGm4eWN18S|DFd(pkU@1J99*-l*|#0%s{%gCUU_Su>i zg@xD?ZpDo5$We|}ka~|7nlBuGFDbLi>{!y5`9;wl{~%VQRU*xV+y8*FGf2y&1;dSF zCW0>B*fk9>;_^TTdA}8+XkvwLYYPFn4aEWCAi%UJ>0)LOUmYmA74g$C1?4UNl-HdJ z{OW-c4u{{L$|j4+nfFL}iHH+(kRHg3=@W!+&L?(GbXKnJ@i*E#?^T4_s0gW;O{0<~ z>4){iTHvR0q7p{Mf0OgQ9U@J+Ff|aDU*#u~MASTGxm;z#RkZij`qFnu{x*$m2->WY zn}C?)UdefBG^KVLs@5qM*&O8;ib3q=fi69^WbGT21H~)2Ht(gaU#PAMiOM*Uio5QV zIi`w+?44o%;QIpMrLNWbpxGijGDhOnPIaw5jO@*3w-s8|7<fAc)YsiRf-bL_fbi zV#@P}Ie={>TD>Hi5WgYP2A%fEp{b_A2ttEB%2EDz`1UZ)rHV=pif(>IaF)98kDuKj zWb60hRZK7Zh-aYDjv(qk_1t5NaF2+x<%2FFy_ckiB;PUJ0(vAV7K&%Nq=9Z3Uya^< zB#V?yj5Nd3)Oc$xy+W!}R8&p^5Tu>HD*!_4T>>kD7yF*AOP-vkNvIl-CLj%gd@%JYO#571$}{__ z(1uikme9&T733f&`H`ltTF1+^_nmk7DTOl#WMqPsKTqA(aRXOGw%2xC?5z?Z)%t>n zwXZ@j+SY`3h^b{u8ltsVHd|{&8<5W0gW5MYU$3)wI!qzU#-Ri=c~hJ-%rdJ#n}kIQ z$-C+Vx<_Hw|GfkT;t0p0S2B8*?smA`G#jq3TB|^C-#-5|d@^ePWYR^>LDqnr8^>RO z?nwy4XuFLt%=CDPTz9CDT*T1Emvd4opwWy%sH_H{n*y--K^zCYv5{d)U0Byz;vjTG z6h?V>Od?ENRUzzfOb!XnC5^D>jQbpm=9}m3{+LWdju~1c%r^);MZ`an8o5IFzb9<< zYq*qM8dng(2-R=r2?V$s(6B~~U_DTug?^NImx8Zz*$U=;h$j@(=}lH}Wx(Wau78n> z@b!>Oab{Rhh|X`2A;5!}YIJ^qsb-%Mw$u@pCj*fBa$Wam(KVeC+5OG{d%N26LkQW@ zpZ598!*%^_HYUs;21+Muuj0K`agbN zF2PPn_Gy`I0pw`zsf%L()Nv$;ZJmKgs=Xjo0S*L{M=bZBwO9U2FF)G18<$>_Wx_x>m(O4~A`rNJDZ=qE zaE3fgmP~cBR!^cW)JuTG9tN8N8Pg95d*E@R3N$(%#-XV5(>0wQ%q>FEr=kh_{9h#v|g=)@b&$JUwLzPjk31bZ6qzy+kjQwKpCRPfdUo zp%4I7_(&@ZlBB4ZUh2b#bU^M2h_{0Cyz%DT(bXYiZ^+c|UhI7++6+`$q*o~`sr+gr z@@BBmW;F~063iFr=~e>*1pr!!Ca6~g`MJZ!(a{w&O5D08uUg_>WH@{ava#ELR@6Y` zaOL*SbvZY3|E6+w)NNEt=Z)%rD!{*QK9JS#K`8FmzOBV5iv73o<@?;iQ(4OoU%aeT Tl2C?l|9j1$^8d^KeFXjsPj>KW literal 0 HcmV?d00001 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 84e497b..29aa7e3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,8 @@ // frontend/src/App.tsx import { useEffect, useState } from 'react' -import { Navigate, Route, Routes } from 'react-router' +import { Navigate, Route, Routes, useNavigate } from 'react-router' +import { NotificationProvider } from './components/Notifications' import LoginPage from './pages/LoginPage' import AppLayout from './components/AppLayout' import DashboardPage from './pages/DashboardPage' @@ -29,6 +30,52 @@ function parseNotificationEvent(event: MessageEvent) { } } +function getNotificationData(notification: Notification) { + const data = (notification as { data?: unknown }).data + + if (!data) { + return {} + } + + if (typeof data === 'string') { + try { + return JSON.parse(data) as Record + } catch { + return {} + } + } + + if (typeof data === 'object') { + return data as Record + } + + return {} +} + +function getUserFromNotification(notification: Notification) { + const data = getNotificationData(notification) + const user = data.user + + if (!user || typeof user !== 'object') { + return null + } + + return user as User +} + +function playNotificationSound(soundName?: string) { + if (!soundName) { + return + } + + const audio = new Audio(`/sounds/notifications/${soundName}.mp3`) + audio.volume = 0.6 + + void audio.play().catch(() => { + // Browser blockiert Audio ggf., bis der User einmal mit der Seite interagiert hat. + }) +} + function RequireRight({ user, right, @@ -46,6 +93,8 @@ function RequireRight({ } function App() { + const navigate = useNavigate() + const [user, setUser] = useState(null) const [isLoading, setIsLoading] = useState(true) @@ -77,10 +126,12 @@ function App() { }) function addVisibleNotification(notification: Notification) { - if (notification.visible === false) { + if (notification.silent) { return } + playNotificationSound(notification.soundName) + setNotifications((currentNotifications) => [ notification, ...currentNotifications.filter((item) => item.id !== notification.id), @@ -151,14 +202,48 @@ function App() { dispatchEntityEvent(notification) } + function handleUserEvent(event: MessageEvent) { + const notification = parseNotificationEvent(event) + + if (!notification) { + return + } + + window.dispatchEvent( + new CustomEvent('user-notification', { + detail: notification, + }), + ) + + const updatedUser = getUserFromNotification(notification) + + if (!updatedUser) { + void loadUser() + return + } + + setUser((currentUser) => { + if (!currentUser || currentUser.id !== updatedUser.id) { + return currentUser + } + + return { + ...currentUser, + ...updatedUser, + } + }) + } + eventSource.addEventListener('notification', handleNotificationEvent) eventSource.addEventListener('operation-event', handleOperationEvent) eventSource.addEventListener('device-event', handleDeviceEvent) + eventSource.addEventListener('user-event', handleUserEvent) return () => { eventSource.removeEventListener('notification', handleNotificationEvent) eventSource.removeEventListener('operation-event', handleOperationEvent) eventSource.removeEventListener('device-event', handleDeviceEvent) + eventSource.removeEventListener('user-event', handleUserEvent) eventSource.close() } }, [user]) @@ -264,6 +349,15 @@ function App() { }).catch(() => undefined) } + function handleNotificationClick(notification: Notification) { + if ( + notification.type === 'operation.created' || + notification.entityType === 'operation' + ) { + navigate('/einsaetze') + } + } + async function handleLogout() { try { await fetch(`${API_URL}/auth/logout`, { @@ -300,101 +394,104 @@ function App() { } return ( - - } - > - - } /> + + + } + > + + } /> - - - - } - /> + + + + } + /> - - - - } - /> + + + + } + /> - - - - } - /> + + + + } + /> - - - - } - /> + + + + } + /> - - - - } - /> + + + + } + /> - } - /> + } + /> - - - - } - /> + + + + } + /> - - - - } - /> + + + + } + /> - } /> + } /> - } /> - - + } /> + + + ) } diff --git a/frontend/src/components/AddressCombobox.tsx b/frontend/src/components/AddressCombobox.tsx index fb3849c..9fb31d3 100644 --- a/frontend/src/components/AddressCombobox.tsx +++ b/frontend/src/components/AddressCombobox.tsx @@ -8,8 +8,10 @@ import { ComboboxOptions, Label, } from '@headlessui/react' +import AddressMapPicker from './AddressMapPicker' +import type { GeoJsonObject } from 'geojson' import { ChevronDownIcon } from '@heroicons/react/20/solid' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:8080' @@ -19,6 +21,16 @@ export type AddressSuggestion = { lat: number lon: number type?: string + geojson?: GeoJsonObject | null +} + +type Coordinates = { + lat: number + lon: number +} + +type ReverseGeocodeResponse = { + suggestion?: AddressSuggestion } type AddressComboboxProps = { @@ -28,6 +40,7 @@ type AddressComboboxProps = { value: string onChange: (value: string) => void placeholder?: string + mapVisible?: boolean } const inputClassName = @@ -37,21 +50,34 @@ function classNames(...classes: Array) { return classes.filter(Boolean).join(' ') } -function buildOpenStreetMapEmbedUrl(address: AddressSuggestion) { - const offset = 0.004 +function toNumber(value: unknown) { + if (typeof value === 'number') { + return value + } - const params = new URLSearchParams({ - bbox: [ - address.lon - offset, - address.lat - offset, - address.lon + offset, - address.lat + offset, - ].join(','), - layer: 'mapnik', - marker: `${address.lat},${address.lon}`, - }) + if (typeof value === 'string') { + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : NaN + } - return `https://www.openstreetmap.org/export/embed.html?${params.toString()}` + return NaN +} + +function normalizeAddressSuggestion( + suggestion: AddressSuggestion, +): AddressSuggestion | null { + const lat = toNumber(suggestion.lat) + const lon = toNumber(suggestion.lon) + + if (!Number.isFinite(lat) || !Number.isFinite(lon)) { + return null + } + + return { + ...suggestion, + lat, + lon, + } } export default function AddressCombobox({ @@ -61,26 +87,223 @@ export default function AddressCombobox({ value, onChange, placeholder = 'Adresse suchen', + mapVisible = true, }: AddressComboboxProps) { const [query, setQuery] = useState(value) const [suggestions, setSuggestions] = useState([]) const [selectedSuggestion, setSelectedSuggestion] = useState(null) const [previewSuggestion, setPreviewSuggestion] = useState(null) + const [markerPosition, setMarkerPosition] = useState<{ + lat: number + lon: number + } | null>(null) + const [mapFocusKey, setMapFocusKey] = useState(0) const [isSearching, setIsSearching] = useState(false) const [searchError, setSearchError] = useState(null) const previousValueRef = useRef(value) + const initializedAddressRef = useRef('') + const previousMapVisibleRef = useRef(mapVisible) + + const reverseGeocodeAbortRef = useRef(null) + const internalValueUpdateRef = useRef(false) useEffect(() => { - if (value !== previousValueRef.current && value !== query) { + if (value !== previousValueRef.current) { setQuery(value) - setSelectedSuggestion(null) - setPreviewSuggestion(null) + + if (internalValueUpdateRef.current) { + internalValueUpdateRef.current = false + previousValueRef.current = value + return + } + + if (value !== query) { + initializedAddressRef.current = '' + + setSelectedSuggestion(null) + setPreviewSuggestion(null) + setMarkerPosition(null) + setMapFocusKey((currentKey) => currentKey + 1) + } } previousValueRef.current = value }, [value, query]) + useEffect(() => { + const becameVisible = mapVisible && !previousMapVisibleRef.current + + previousMapVisibleRef.current = mapVisible + + if (!becameVisible) { + return + } + + if (markerPosition) { + setMapFocusKey((currentKey) => currentKey + 1) + return + } + + initializedAddressRef.current = '' + + void loadInitialMarkerForAddress(value) + }, [mapVisible, markerPosition, value]) + + useEffect(() => { + const trimmedValue = value.trim() + + if (trimmedValue.length < 3) { + initializedAddressRef.current = '' + return + } + + if (markerPosition) { + return + } + + const abortController = new AbortController() + + void loadInitialMarkerForAddress(trimmedValue, abortController.signal) + + return () => { + abortController.abort() + } + }, [value, markerPosition]) + + async function loadInitialMarkerForAddress( + address: string, + signal?: AbortSignal, + ) { + const trimmedAddress = address.trim() + + if (trimmedAddress.length < 3) { + return + } + + if (initializedAddressRef.current === trimmedAddress && markerPosition) { + return + } + + try { + const response = await fetch( + `${API_URL}/address-search?q=${encodeURIComponent(trimmedAddress)}`, + { + credentials: 'include', + signal, + }, + ) + + if (!response.ok) { + return + } + + const data = await response.json() + + const firstSuggestion = ((data.suggestions ?? []) as AddressSuggestion[]) + .map(normalizeAddressSuggestion) + .find((suggestion): suggestion is AddressSuggestion => suggestion !== null) + + if (!firstSuggestion) { + return + } + + const reverseSuggestion = await reverseGeocodeCoordinates({ + lat: firstSuggestion.lat, + lon: firstSuggestion.lon, + }) + + const nextSuggestion: AddressSuggestion = { + ...firstSuggestion, + geojson: reverseSuggestion?.geojson ?? firstSuggestion.geojson ?? null, + } + + initializedAddressRef.current = trimmedAddress + + setSelectedSuggestion(nextSuggestion) + setPreviewSuggestion(nextSuggestion) + setMarkerPosition({ + lat: nextSuggestion.lat, + lon: nextSuggestion.lon, + }) + setMapFocusKey((currentKey) => currentKey + 1) + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') { + return + } + } + } + + async function reverseGeocodeCoordinates( + coordinates: Coordinates, + ): Promise { + reverseGeocodeAbortRef.current?.abort() + + const abortController = new AbortController() + reverseGeocodeAbortRef.current = abortController + + try { + const response = await fetch( + `${API_URL}/address-reverse?lat=${encodeURIComponent( + coordinates.lat, + )}&lon=${encodeURIComponent(coordinates.lon)}`, + { + credentials: 'include', + signal: abortController.signal, + }, + ) + + if (!response.ok) { + return null + } + + const data = (await response.json()) as ReverseGeocodeResponse + + if (!data.suggestion) { + return null + } + + const normalizedSuggestion = normalizeAddressSuggestion(data.suggestion) + + if (!normalizedSuggestion) { + return null + } + + return { + ...normalizedSuggestion, + lat: coordinates.lat, + lon: coordinates.lon, + } + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') { + return null + } + + return null + } + } + + async function handleMapPositionChange(coordinates: Coordinates) { + setMarkerPosition(coordinates) + setSelectedSuggestion(null) + setPreviewSuggestion(null) + + const suggestion = await reverseGeocodeCoordinates(coordinates) + + if (!suggestion) { + return + } + + initializedAddressRef.current = suggestion.label + internalValueUpdateRef.current = true + + setQuery(suggestion.label) + setSelectedSuggestion(suggestion) + setPreviewSuggestion(suggestion) + + onChange(suggestion.label) + } + useEffect(() => { const trimmedQuery = query.trim() @@ -113,10 +336,14 @@ export default function AddressCombobox({ } const data = await response.json() - const nextSuggestions = (data.suggestions ?? []) as AddressSuggestion[] + const nextSuggestions = ((data.suggestions ?? []) as AddressSuggestion[]) + .map(normalizeAddressSuggestion) + .filter((suggestion): suggestion is AddressSuggestion => suggestion !== null) + + const nextPreview = nextSuggestions[0] ?? null setSuggestions(nextSuggestions) - setPreviewSuggestion((currentPreview) => currentPreview ?? nextSuggestions[0] ?? null) + setPreviewSuggestion((currentPreview) => currentPreview ?? nextPreview) } catch (error) { if (error instanceof DOMException && error.name === 'AbortError') { return @@ -140,33 +367,58 @@ export default function AddressCombobox({ } }, [query]) - const mapUrl = useMemo(() => { - if (!previewSuggestion) { - return null - } - - return buildOpenStreetMapEmbedUrl(previewSuggestion) - }, [previewSuggestion]) - function handleInputChange(nextValue: string) { + reverseGeocodeAbortRef.current?.abort() + internalValueUpdateRef.current = false + initializedAddressRef.current = '' + setQuery(nextValue) setSelectedSuggestion(null) setPreviewSuggestion(null) + setMarkerPosition(null) onChange(nextValue) } - function handleSelect(suggestion: AddressSuggestion | null) { - setSelectedSuggestion(suggestion) + async function handleSelect(suggestion: AddressSuggestion | null) { + const normalizedSuggestion = suggestion + ? normalizeAddressSuggestion(suggestion) + : null - if (!suggestion) { + setSelectedSuggestion(normalizedSuggestion) + + if (!normalizedSuggestion) { return } - setQuery(suggestion.label) - setPreviewSuggestion(suggestion) - onChange(suggestion.label) + setMarkerPosition({ + lat: normalizedSuggestion.lat, + lon: normalizedSuggestion.lon, + }) + setMapFocusKey((currentKey) => currentKey + 1) + + const reverseSuggestion = await reverseGeocodeCoordinates({ + lat: normalizedSuggestion.lat, + lon: normalizedSuggestion.lon, + }) + + const nextSuggestion: AddressSuggestion = { + ...normalizedSuggestion, + label: reverseSuggestion?.label ?? normalizedSuggestion.label, + geojson: reverseSuggestion?.geojson ?? normalizedSuggestion.geojson ?? null, + } + + initializedAddressRef.current = nextSuggestion.label + internalValueUpdateRef.current = true + + setQuery(nextSuggestion.label) + setSelectedSuggestion(nextSuggestion) + setPreviewSuggestion(nextSuggestion) + + onChange(nextSuggestion.label) } + const mapKey = `${mapVisible ? 'visible' : 'hidden'}-${mapFocusKey}` + return (

@@ -196,7 +448,7 @@ export default function AddressCombobox({ {(suggestions.length > 0 || isSearching || searchError) && ( {isSearching && (
@@ -230,22 +482,14 @@ export default function AddressCombobox({
- {mapUrl && previewSuggestion && ( -
-