Tutorial 6: Search, Filter & Sort¶
Pages: 2
📖 Page 1: Advanced Filters¶
Update PatientService¶
Add to PatientService.java:
public List<PatientDTO> filterPatients(String name, String gender,
String bloodGroup, String status,
Integer minAge, Integer maxAge) {
List<PatientDTO> patients = getAllPatients();
return patients.stream()
// Name filter (case-insensitive)
.filter(p -> name == null || name.isEmpty() ||
p.getFullName().toLowerCase().contains(name.toLowerCase()))
// Gender filter
.filter(p -> gender == null || gender.isEmpty() ||
p.getGender().equalsIgnoreCase(gender))
// Blood group filter
.filter(p -> bloodGroup == null || bloodGroup.isEmpty() ||
bloodGroup.equals(p.getBloodGroup()))
// Status filter
.filter(p -> {
if (status == null || status.isEmpty()) return true;
return "active".equals(status) ? p.getActive() : !p.getActive();
})
// Age range filter
.filter(p -> {
if (p.getBirthDate() == null) return true;
int age = LocalDate.now().getYear() - p.getBirthDate().getYear();
boolean minOk = minAge == null || age >= minAge;
boolean maxOk = maxAge == null || age <= maxAge;
return minOk && maxOk;
})
.collect(Collectors.toList());
}
// Sort patients
public List<PatientDTO> sortPatients(List<PatientDTO> patients,
String sortBy, String order) {
if (sortBy == null || sortBy.isEmpty()) return patients;
Comparator<PatientDTO> comparator;
switch (sortBy) {
case "name":
comparator = Comparator.comparing(PatientDTO::getFullName);
break;
case "age":
comparator = Comparator.comparing(PatientDTO::getBirthDate);
break;
case "gender":
comparator = Comparator.comparing(PatientDTO::getGender);
break;
default:
return patients;
}
if ("desc".equals(order)) {
comparator = comparator.reversed();
}
return patients.stream()
.sorted(comparator)
.collect(Collectors.toList());
}
Update Controller¶
Update PatientController listPatients method:
@GetMapping
public String listPatients(
@RequestParam(required = false) String name,
@RequestParam(required = false) String gender,
@RequestParam(required = false) String bloodGroup,
@RequestParam(required = false) String status,
@RequestParam(required = false) Integer minAge,
@RequestParam(required = false) Integer maxAge,
@RequestParam(required = false) String sortBy,
@RequestParam(required = false) String order,
Model model) {
// Filter
List<PatientDTO> patients = patientService.filterPatients(
name, gender, bloodGroup, status, minAge, maxAge
);
// Sort
patients = patientService.sortPatients(patients, sortBy, order);
// Add to model
model.addAttribute("patients", patients);
model.addAttribute("name", name);
model.addAttribute("gender", gender);
model.addAttribute("bloodGroup", bloodGroup);
model.addAttribute("status", status);
model.addAttribute("minAge", minAge);
model.addAttribute("maxAge", maxAge);
model.addAttribute("sortBy", sortBy);
model.addAttribute("order", order);
return "patients/list";
}
📖 Page 2: Enhanced UI¶
Update list.html with All Filters¶
Replace filter section in templates/patients/list.html:
<form class="row g-2 mb-3" method="get">
<!-- Name Search -->
<div class="col-md-3">
<input type="text" name="name" class="form-control"
placeholder="Search name" th:value="${name}">
</div>
<!-- Gender Filter -->
<div class="col-md-2">
<select name="gender" class="form-control">
<option value="">All Genders</option>
<option value="male" th:selected="${gender == 'male'}">Male</option>
<option value="female" th:selected="${gender == 'female'}">Female</option>
<option value="other" th:selected="${gender == 'other'}">Other</option>
</select>
</div>
<!-- Blood Group Filter -->
<div class="col-md-2">
<select name="bloodGroup" class="form-control">
<option value="">All Blood Groups</option>
<option value="A+" th:selected="${bloodGroup == 'A+'}">A+</option>
<option value="A-" th:selected="${bloodGroup == 'A-'}">A-</option>
<option value="B+" th:selected="${bloodGroup == 'B+'}">B+</option>
<option value="B-" th:selected="${bloodGroup == 'B-'}">B-</option>
<option value="AB+" th:selected="${bloodGroup == 'AB+'}">AB+</option>
<option value="AB-" th:selected="${bloodGroup == 'AB-'}">AB-</option>
<option value="O+" th:selected="${bloodGroup == 'O+'}">O+</option>
<option value="O-" th:selected="${bloodGroup == 'O-'}">O-</option>
</select>
</div>
<!-- Status Filter -->
<div class="col-md-2">
<select name="status" class="form-control">
<option value="">All Status</option>
<option value="active" th:selected="${status == 'active'}">Active</option>
<option value="inactive" th:selected="${status == 'inactive'}">Inactive</option>
</select>
</div>
<!-- Age Range -->
<div class="col-md-1">
<input type="number" name="minAge" class="form-control"
placeholder="Min" th:value="${minAge}">
</div>
<div class="col-md-1">
<input type="number" name="maxAge" class="form-control"
placeholder="Max" th:value="${maxAge}">
</div>
<!-- Actions -->
<div class="col-md-1">
<button type="submit" class="btn btn-primary w-100">Filter</button>
</div>
</form>
<!-- Active Filter Badges -->
<div class="mb-3" th:if="${name != null or gender != null or bloodGroup != null}">
<span th:if="${name != null}" class="badge bg-info">
Name: <span th:text="${name}"></span>
<a th:href="@{/patients(gender=${gender},bloodGroup=${bloodGroup})}"
class="text-white">×</a>
</span>
<span th:if="${gender != null}" class="badge bg-info">
Gender: <span th:text="${gender}"></span>
<a th:href="@{/patients(name=${name},bloodGroup=${bloodGroup})}"
class="text-white">×</a>
</span>
<span th:if="${bloodGroup != null}" class="badge bg-info">
Blood: <span th:text="${bloodGroup}"></span>
<a th:href="@{/patients(name=${name},gender=${gender})}"
class="text-white">×</a>
</span>
<a href="/patients" class="badge bg-secondary">Clear All</a>
</div>
<!-- Sort Controls -->
<div class="mb-2">
<span>Sort by:</span>
<a th:href="@{/patients(name=${name},gender=${gender},sortBy='name',order='asc')}"
class="btn btn-sm btn-outline-primary">Name ↑</a>
<a th:href="@{/patients(name=${name},gender=${gender},sortBy='name',order='desc')}"
class="btn btn-sm btn-outline-primary">Name ↓</a>
<a th:href="@{/patients(name=${name},gender=${gender},sortBy='age',order='asc')}"
class="btn btn-sm btn-outline-primary">Age ↑</a>
<a th:href="@{/patients(name=${name},gender=${gender},sortBy='age',order='desc')}"
class="btn btn-sm btn-outline-primary">Age ↓</a>
</div>
Add Blood Group to Form¶
In templates/patients/form.html, add after phone:
<div class="col-md-6 mb-3">
<label>Blood Group</label>
<select th:field="*{bloodGroup}" class="form-control">
<option value="">Select</option>
<option value="A+">A+</option>
<option value="A-">A-</option>
<option value="B+">B+</option>
<option value="B-">B-</option>
<option value="AB+">AB+</option>
<option value="AB-">AB-</option>
<option value="O+">O+</option>
<option value="O-">O-</option>
</select>
</div>
✅ All Filters Working¶
- Name search (case-insensitive)
- Gender filter (male/female/other)
- Blood group filter (8 types)
- Status filter (active/inactive)
- Age range filter (min/max)
- Sort by name/age (asc/desc)
- Filter badges with remove
- Clear all filters
🚀 Next¶
Filters complete! Now add pagination.
→ Tutorial 7: Pagination & UI Components
💡 Quick Tips¶
Stream API
.filter() chains multiple conditions with AND logic
Comparator
.reversed() flips sort order for descending
URL Parameters
Preserve filters when sorting by passing all params