Skip to content

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