Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
package com.statusneo.vms.cache;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.stereotype.Component;

import com.statusneo.vms.model.Employee;
import com.statusneo.vms.repository.EmployeeRepository;
import com.statusneo.vms.util.TrieNode;

import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
* Lightweight in-memory cache of employees (name/email) used by UI host-search.
* Added search(...) helper so UI queries use the cache rather than hitting DB
* or external APIs.
*/
@Component
public class EmployeeNameCache {

Expand All @@ -40,7 +39,8 @@ public void initializeCache() {
}

public void insert(String name) {
if (name == null || name.isBlank()) return;
if (name == null || name.isBlank())
return;
TrieNode node = root;
for (char c : name.toLowerCase().toCharArray()) {
node = node.getChildren().computeIfAbsent(c, k -> new TrieNode());
Expand All @@ -50,11 +50,13 @@ public void insert(String name) {
}

public List<String> getEmployeeNamesByPrefix(String prefix) {
if (prefix == null) prefix = "";
if (prefix == null)
prefix = "";
TrieNode node = root;
for (char c : prefix.toLowerCase().toCharArray()) {
node = node.getChildren().get(c);
if (node == null) return Collections.emptyList();
if (node == null)
return Collections.emptyList();
}

// Use LinkedHashSet to preserve insertion order and avoid duplicates
Expand All @@ -70,17 +72,20 @@ public List<String> getEmployeeNamesByPrefix(String prefix) {
}

private void collectNames(TrieNode node, Set<String> results) {
if (results.size() >= MAX_SUGGESTIONS) return;
if (results.size() >= MAX_SUGGESTIONS)
return;
if (node.isEndOfWord()) {
// add originals for this terminal node
for (String orig : node.getOriginals()) {
if (results.size() >= MAX_SUGGESTIONS) break;
if (results.size() >= MAX_SUGGESTIONS)
break;
results.add(orig);
}
}

for (Map.Entry<Character, TrieNode> entry : node.getChildren().entrySet()) {
if (results.size() >= MAX_SUGGESTIONS) break;
if (results.size() >= MAX_SUGGESTIONS)
break;
collectNames(entry.getValue(), results);
}
}
Expand All @@ -96,4 +101,40 @@ public void bulkInsert(Collection<String> names) {
}
}
}

// New code starts here
private final Map<Long, Employee> byId = new ConcurrentHashMap<>();

/**
* Search cached employees by name/email/identifier. Case-insensitive substring
* match.
* If query is null/empty returns a small default list (first 25).
*/
public List<Employee> search(String query) {
Collection<Employee> all = byId.values();
if (query == null || query.isBlank()) {
return all.stream()
.sorted(Comparator.comparing(Employee::getName, Comparator.nullsLast(String::compareToIgnoreCase)))
.limit(25)
.collect(Collectors.toList());
}
String q = query.toLowerCase(Locale.ROOT).trim();
return all.stream()
.filter(e -> {
if (e == null)
return false;
String name = e.getName() == null ? "" : e.getName().toLowerCase(Locale.ROOT);
String email = e.getEmail() == null ? "" : e.getEmail().toLowerCase(Locale.ROOT);
return name.contains(q) || email.contains(q);
})
.sorted(Comparator.comparing(Employee::getName, Comparator.nullsLast(String::compareToIgnoreCase)))
.limit(25)
.collect(Collectors.toList());
}

// Optional convenience
public Optional<Employee> getById(Long id) {
return Optional.ofNullable(byId.get(id));
}
// New code ends here
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,61 @@
*/
package com.statusneo.vms.controller;

import com.statusneo.vms.model.Employee;
import com.statusneo.vms.service.EmployeeService;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;
import com.statusneo.vms.cache.EmployeeNameCache;
import com.statusneo.vms.model.Employee;

/**
* Expose host search endpoint backed by EmployeeNameCache so UI queries use
* cache.
*/
@Controller
public class HomeController {
private final EmployeeService employeeService;

public HomeController(EmployeeService employeeService) {
this.employeeService = employeeService;
private final EmployeeNameCache employeeNameCache;

@Autowired
public HomeController(EmployeeNameCache employeeNameCache /* , other deps if present */) {
this.employeeNameCache = employeeNameCache;
}

/**
* HTMX endpoint used by the host-search box.
* Returns a small fragment containing search results. The fragment has
* id="host-results"
* so client-side JS/HTMX can reveal it after swap.
*
* Example request: GET /api/hosts/search?hostSearch=anas
*/
@GetMapping("/api/hosts/search")
public String searchHosts(@RequestParam(name = "hostSearch", required = false) String hostSearch, Model model) {
List<Employee> results = employeeNameCache.search(hostSearch);
model.addAttribute("hosts", results);
// return Thymeleaf fragment - ensure fragment name "results" exists in template
return "host-search-results :: results";
}

@GetMapping("/search-employees")
public String employees(@RequestParam(value = "hostSearch", required = false) String hostSearch,
@RequestParam(value = "employee", required = false) String employee,
@RequestParam(value = "query", required = false) String query,
Model model) {
@RequestParam(value = "employee", required = false) String employee,
@RequestParam(value = "query", required = false) String query,
Model model) {
// Prefer hostSearch (used by index.jte), then employee, then query
String q = (hostSearch != null && !hostSearch.isBlank()) ? hostSearch :
((employee != null && !employee.isBlank()) ? employee : (query == null ? "" : query));
String q = (hostSearch != null && !hostSearch.isBlank()) ? hostSearch
: ((employee != null && !employee.isBlank()) ? employee : (query == null ? "" : query));

if (q.isBlank()) {
return "employees";
}

List<Employee> employees = employeeService.searchEmployeesByName(q);
List<Employee> employees = employeeNameCache.search(q);
model.addAttribute("employees", employees);
return "employees";
}
Expand Down
21 changes: 11 additions & 10 deletions web-backend/src/main/resources/templates/host-search-results.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<div th:fragment="hostResults" xmlns:th="http://www.w3.org/1999/xhtml">
<div th:if="${hosts != null and !hosts.isEmpty()}">
<div th:each="host : ${hosts}"
th:attr="onclick='selectHost(' + ${host.id} + ', \'' + ${host.name} + '\', \'' + ${host.email} + '\')'"
class="px-4 py-3 hover:bg-gray-50 cursor-pointer border-b border-gray-100 last:border-b-0">
<div class="font-medium text-gray-900" th:text="${host.name}">John Smith</div>
<div class="text-sm text-gray-500" th:text="${host.email}">john.smith@company.com</div>
<!-- fragment returned for HTMX host-search; must contain element with id="host-results" -->
<div th:fragment="results">
<div id="host-results" class="bg-white border rounded-md mt-1 shadow-lg max-h-72 overflow-auto">
<ul class="divide-y">
<li th:each="emp : ${hosts}" class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
th:onclick="'selectHost(' + ${emp.id} + ', \'' + ${emp.name} + '\', \'' + ${emp.email} + '\')'">
<div class="text-sm font-medium" th:text="${emp.name}">Employee Name</div>
<div class="text-xs text-gray-500" th:text="${emp.email}">email@example.com</div>
</li>
<li th:if="${#lists.isEmpty(hosts)}" class="px-4 py-2 text-sm text-gray-500">No results</li>
</ul>
</div>
</div>
<div th:if="${hosts == null or hosts.isEmpty()}"
class="px-4 py-3 text-gray-500 text-center">
No employee found
</div>
</div>
Loading