Merge pull request #320 from RecentRunner/fix-desktop-launch
Desktop fixes
This commit is contained in:
@@ -10,7 +10,7 @@ public class ActivityLoggingFilterRegistrationConfig {
|
||||
@Bean
|
||||
public FilterRegistrationBean<ActivityLoggingFilter> activityLoggingFilterRegistration(ActivityLoggingFilter activityLoggingFilter) {
|
||||
FilterRegistrationBean<ActivityLoggingFilter> registrationBean = new FilterRegistrationBean<>(activityLoggingFilter);
|
||||
registrationBean.setEnabled(false);
|
||||
registrationBean.setEnabled(true);
|
||||
return registrationBean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.petshop.backend.controller;
|
||||
|
||||
import com.petshop.backend.dto.activity.ActivityLogResponse;
|
||||
import com.petshop.backend.service.ActivityLogService;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/activity-logs")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public class ActivityLogController {
|
||||
|
||||
private final ActivityLogService activityLogService;
|
||||
|
||||
public ActivityLogController(ActivityLogService activityLogService) {
|
||||
this.activityLogService = activityLogService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<ActivityLogResponse>> getActivityLogs(
|
||||
@RequestParam(defaultValue = "2000") int limit,
|
||||
@RequestParam(required = false) Long storeId,
|
||||
@RequestParam(required = false) String role,
|
||||
@RequestParam(required = false) String search,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
|
||||
int safeLimit = Math.min(Math.max(1, limit), 10000);
|
||||
return ResponseEntity.ok(activityLogService.getLogs(safeLimit, storeId, role, search, startDate, endDate));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.petshop.backend.dto.activity;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class ActivityLogResponse {
|
||||
private Long logId;
|
||||
private Long userId;
|
||||
private String username;
|
||||
private String fullName;
|
||||
private String role;
|
||||
private Long storeId;
|
||||
private String storeName;
|
||||
private String usernameSnapshot;
|
||||
private String fullNameSnapshot;
|
||||
private String roleSnapshot;
|
||||
private String storeNameSnapshot;
|
||||
private String activity;
|
||||
private LocalDateTime logTimestamp;
|
||||
|
||||
public ActivityLogResponse() {
|
||||
}
|
||||
|
||||
public Long getLogId() {
|
||||
return logId;
|
||||
}
|
||||
|
||||
public void setLogId(Long logId) {
|
||||
this.logId = logId;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getFullName() {
|
||||
return fullName;
|
||||
}
|
||||
|
||||
public void setFullName(String fullName) {
|
||||
this.fullName = fullName;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public void setRole(String role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
public Long getStoreId() {
|
||||
return storeId;
|
||||
}
|
||||
|
||||
public void setStoreId(Long storeId) {
|
||||
this.storeId = storeId;
|
||||
}
|
||||
|
||||
public String getStoreName() {
|
||||
return storeName;
|
||||
}
|
||||
|
||||
public void setStoreName(String storeName) {
|
||||
this.storeName = storeName;
|
||||
}
|
||||
|
||||
public String getUsernameSnapshot() {
|
||||
return usernameSnapshot;
|
||||
}
|
||||
|
||||
public void setUsernameSnapshot(String usernameSnapshot) {
|
||||
this.usernameSnapshot = usernameSnapshot;
|
||||
}
|
||||
|
||||
public String getFullNameSnapshot() {
|
||||
return fullNameSnapshot;
|
||||
}
|
||||
|
||||
public void setFullNameSnapshot(String fullNameSnapshot) {
|
||||
this.fullNameSnapshot = fullNameSnapshot;
|
||||
}
|
||||
|
||||
public String getRoleSnapshot() {
|
||||
return roleSnapshot;
|
||||
}
|
||||
|
||||
public void setRoleSnapshot(String roleSnapshot) {
|
||||
this.roleSnapshot = roleSnapshot;
|
||||
}
|
||||
|
||||
public String getStoreNameSnapshot() {
|
||||
return storeNameSnapshot;
|
||||
}
|
||||
|
||||
public void setStoreNameSnapshot(String storeNameSnapshot) {
|
||||
this.storeNameSnapshot = storeNameSnapshot;
|
||||
}
|
||||
|
||||
public String getActivity() {
|
||||
return activity;
|
||||
}
|
||||
|
||||
public void setActivity(String activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
public LocalDateTime getLogTimestamp() {
|
||||
return logTimestamp;
|
||||
}
|
||||
|
||||
public void setLogTimestamp(LocalDateTime logTimestamp) {
|
||||
this.logTimestamp = logTimestamp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.petshop.backend.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.Immutable;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Immutable
|
||||
@Table(name = "activityLog")
|
||||
public class ActivityLog {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long logId;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "userId", nullable = false)
|
||||
private User user;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "storeId")
|
||||
private StoreLocation store;
|
||||
|
||||
@Column(length = 50)
|
||||
private String usernameSnapshot;
|
||||
|
||||
@Column(length = 100)
|
||||
private String fullNameSnapshot;
|
||||
|
||||
@Column(length = 20)
|
||||
private String roleSnapshot;
|
||||
|
||||
@Column(length = 100)
|
||||
private String storeNameSnapshot;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
private String activity;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime logTimestamp = LocalDateTime.now();
|
||||
|
||||
public ActivityLog() {
|
||||
}
|
||||
|
||||
public ActivityLog(Long logId, User user, String activity, LocalDateTime logTimestamp) {
|
||||
this.logId = logId;
|
||||
this.user = user;
|
||||
this.activity = activity;
|
||||
this.logTimestamp = logTimestamp;
|
||||
}
|
||||
|
||||
public Long getLogId() {
|
||||
return logId;
|
||||
}
|
||||
|
||||
public void setLogId(Long logId) {
|
||||
this.logId = logId;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public StoreLocation getStore() {
|
||||
return store;
|
||||
}
|
||||
|
||||
public void setStore(StoreLocation store) {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
public String getUsernameSnapshot() {
|
||||
return usernameSnapshot;
|
||||
}
|
||||
|
||||
public void setUsernameSnapshot(String usernameSnapshot) {
|
||||
this.usernameSnapshot = usernameSnapshot;
|
||||
}
|
||||
|
||||
public String getFullNameSnapshot() {
|
||||
return fullNameSnapshot;
|
||||
}
|
||||
|
||||
public void setFullNameSnapshot(String fullNameSnapshot) {
|
||||
this.fullNameSnapshot = fullNameSnapshot;
|
||||
}
|
||||
|
||||
public String getRoleSnapshot() {
|
||||
return roleSnapshot;
|
||||
}
|
||||
|
||||
public void setRoleSnapshot(String roleSnapshot) {
|
||||
this.roleSnapshot = roleSnapshot;
|
||||
}
|
||||
|
||||
public String getStoreNameSnapshot() {
|
||||
return storeNameSnapshot;
|
||||
}
|
||||
|
||||
public void setStoreNameSnapshot(String storeNameSnapshot) {
|
||||
this.storeNameSnapshot = storeNameSnapshot;
|
||||
}
|
||||
|
||||
public String getActivity() {
|
||||
return activity;
|
||||
}
|
||||
|
||||
public void setActivity(String activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
public LocalDateTime getLogTimestamp() {
|
||||
return logTimestamp;
|
||||
}
|
||||
|
||||
public void setLogTimestamp(LocalDateTime logTimestamp) {
|
||||
this.logTimestamp = logTimestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ActivityLog that = (ActivityLog) o;
|
||||
return Objects.equals(logId, that.logId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(logId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ActivityLog{" +
|
||||
"logId=" + logId +
|
||||
", user=" + user +
|
||||
", activity='" + activity + '\'' +
|
||||
", logTimestamp=" + logTimestamp +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.petshop.backend.repository;
|
||||
|
||||
import com.petshop.backend.entity.ActivityLog;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface ActivityLogRepository extends JpaRepository<ActivityLog, Long>, JpaSpecificationExecutor<ActivityLog> {
|
||||
boolean existsByUser_Id(Long userId);
|
||||
|
||||
@Query("select a from ActivityLog a order by a.logTimestamp desc, a.logId desc")
|
||||
List<ActivityLog> findRecent(Pageable pageable);
|
||||
}
|
||||
@@ -43,8 +43,8 @@ public interface PetRepository extends JpaRepository<Pet, Long> {
|
||||
@Query("SELECT p FROM Pet p WHERE p.id = :id")
|
||||
Optional<Pet> findByIdForUpdate(@Param("id") Long id);
|
||||
|
||||
@Query("SELECT p FROM Pet p WHERE " +
|
||||
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
|
||||
@Query("SELECT p FROM Pet p LEFT JOIN p.owner o WHERE " +
|
||||
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(o.firstName, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(o.lastName, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(CONCAT(COALESCE(o.firstName, ''), ' ', COALESCE(o.lastName, ''))) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
|
||||
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
|
||||
"(:breed IS NULL OR LOWER(COALESCE(p.petBreed, '')) = LOWER(:breed)) AND " +
|
||||
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status)) AND " +
|
||||
|
||||
@@ -1,48 +1,174 @@
|
||||
package com.petshop.backend.service;
|
||||
|
||||
import com.petshop.backend.dto.activity.ActivityLogResponse;
|
||||
import com.petshop.backend.entity.ActivityLog;
|
||||
import com.petshop.backend.entity.StoreLocation;
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.repository.ActivityLogRepository;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class ActivityLogService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger("activity");
|
||||
private static final Logger log = LoggerFactory.getLogger(ActivityLogService.class);
|
||||
|
||||
private final ActivityLogRepository activityLogRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public ActivityLogService(UserRepository userRepository) {
|
||||
public ActivityLogService(ActivityLogRepository activityLogRepository, UserRepository userRepository) {
|
||||
this.activityLogRepository = activityLogRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Async
|
||||
@Transactional
|
||||
public void record(Long userId, String activity) {
|
||||
if (userId == null || activity == null || activity.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
User user = userRepository.findById(userId).orElse(null);
|
||||
if (user == null) {
|
||||
User managedUser = userRepository.findById(userId).orElse(null);
|
||||
if (managedUser == null) {
|
||||
return;
|
||||
}
|
||||
StoreLocation store = user.getPrimaryStore();
|
||||
String role = user.getRole() != null ? user.getRole().name() : "UNKNOWN";
|
||||
String storeName = store != null ? store.getStoreName() : "no store";
|
||||
log.info("{} | {} | {} | {}", role, user.getUsername(), storeName, activity.trim());
|
||||
StoreLocation store = managedUser.getPrimaryStore();
|
||||
ActivityLog entry = new ActivityLog();
|
||||
entry.setUser(managedUser);
|
||||
entry.setStore(store);
|
||||
entry.setUsernameSnapshot(managedUser.getUsername());
|
||||
entry.setFullNameSnapshot(resolveFullName(managedUser));
|
||||
entry.setRoleSnapshot(managedUser.getRole() != null ? managedUser.getRole().name() : null);
|
||||
entry.setStoreNameSnapshot(store != null ? store.getStoreName() : null);
|
||||
entry.setActivity(activity.trim());
|
||||
activityLogRepository.save(entry);
|
||||
log.info("[ACTIVITY] {} | {} | {} | {}",
|
||||
entry.getRoleSnapshot(),
|
||||
entry.getUsernameSnapshot(),
|
||||
entry.getStoreNameSnapshot() != null ? entry.getStoreNameSnapshot() : "no store",
|
||||
entry.getActivity());
|
||||
} catch (Exception ex) {
|
||||
log.warn("Failed to record activity", ex);
|
||||
log.warn("Failed to persist activity log", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Async
|
||||
public void record(User user, String activity) {
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
record(user.getId(), activity);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ActivityLogResponse> getLogs(int limit, Long storeId, String role, String search, LocalDate startDate, LocalDate endDate) {
|
||||
Specification<ActivityLog> spec = (root, query, cb) -> {
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
|
||||
if (storeId != null) {
|
||||
predicates.add(cb.equal(root.get("store").get("storeId"), storeId));
|
||||
}
|
||||
|
||||
if (role != null && !role.isBlank()) {
|
||||
predicates.add(cb.equal(root.get("roleSnapshot"), role));
|
||||
}
|
||||
|
||||
if (search != null && !search.isBlank()) {
|
||||
String pattern = "%" + search.toLowerCase() + "%";
|
||||
Predicate searchPredicate = cb.or(
|
||||
cb.like(cb.lower(root.get("activity")), pattern),
|
||||
cb.like(cb.lower(root.get("fullNameSnapshot")), pattern),
|
||||
cb.like(cb.lower(root.get("usernameSnapshot")), pattern)
|
||||
);
|
||||
predicates.add(searchPredicate);
|
||||
}
|
||||
|
||||
if (startDate != null) {
|
||||
predicates.add(cb.greaterThanOrEqualTo(root.get("logTimestamp"), startDate.atStartOfDay()));
|
||||
}
|
||||
|
||||
if (endDate != null) {
|
||||
predicates.add(cb.lessThan(root.get("logTimestamp"), endDate.plusDays(1).atStartOfDay()));
|
||||
}
|
||||
|
||||
return cb.and(predicates.toArray(new Predicate[0]));
|
||||
};
|
||||
|
||||
PageRequest pageRequest = PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "logTimestamp", "logId"));
|
||||
return activityLogRepository.findAll(spec, pageRequest).stream()
|
||||
.map(this::toResponse)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ActivityLogResponse> getLogs(int limit, Long storeId, String role, String search) {
|
||||
return getLogs(limit, storeId, role, search, null, null);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ActivityLogResponse> getLogs(int limit) {
|
||||
return getLogs(limit, null, null, null, null, null);
|
||||
}
|
||||
|
||||
private ActivityLogResponse toResponse(ActivityLog entry) {
|
||||
ActivityLogResponse response = new ActivityLogResponse();
|
||||
response.setLogId(entry.getLogId());
|
||||
|
||||
if (entry.getUser() != null) {
|
||||
response.setUserId(entry.getUser().getId());
|
||||
response.setUsername(firstNonBlank(entry.getUsernameSnapshot(), entry.getUser().getUsername()));
|
||||
response.setFullName(firstNonBlank(entry.getFullNameSnapshot(), resolveFullName(entry.getUser())));
|
||||
response.setRole(firstNonBlank(entry.getRoleSnapshot(), entry.getUser().getRole() != null ? entry.getUser().getRole().name() : null));
|
||||
}
|
||||
|
||||
StoreLocation store = entry.getStore();
|
||||
if (store != null) {
|
||||
response.setStoreId(store.getStoreId());
|
||||
response.setStoreName(firstNonBlank(entry.getStoreNameSnapshot(), store.getStoreName()));
|
||||
}
|
||||
|
||||
response.setUsernameSnapshot(entry.getUsernameSnapshot());
|
||||
response.setFullNameSnapshot(entry.getFullNameSnapshot());
|
||||
response.setRoleSnapshot(entry.getRoleSnapshot());
|
||||
response.setStoreNameSnapshot(entry.getStoreNameSnapshot());
|
||||
|
||||
response.setActivity(entry.getActivity());
|
||||
response.setLogTimestamp(entry.getLogTimestamp());
|
||||
return response;
|
||||
}
|
||||
|
||||
private String resolveFullName(User user) {
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
if (user.getFullName() != null && !user.getFullName().isBlank()) {
|
||||
return user.getFullName();
|
||||
}
|
||||
String first = user.getFirstName();
|
||||
String last = user.getLastName();
|
||||
if (first == null || first.isBlank()) {
|
||||
return last;
|
||||
}
|
||||
if (last == null || last.isBlank()) {
|
||||
return first;
|
||||
}
|
||||
return first.trim() + " " + last.trim();
|
||||
}
|
||||
|
||||
private String firstNonBlank(String preferred, String fallback) {
|
||||
if (preferred != null && !preferred.isBlank()) {
|
||||
return preferred;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,8 +90,9 @@ public class ChatRealtimeClient implements WebSocket.Listener {
|
||||
for (ConversationResponse conv : globalConversations.values()) {
|
||||
if ("CLOSED".equals(conv.getStatus())) continue;
|
||||
|
||||
// Needs pickup
|
||||
if (conv.getHumanRequestedAt() != null && conv.getStaffId() == null) {
|
||||
// Needs pickup - only if we haven't already replied
|
||||
if (conv.getHumanRequestedAt() != null && conv.getStaffId() == null
|
||||
&& (currentUserId == null || !currentUserId.equals(conv.getLastSenderId()))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user