Skip to content

Tutorial 5: Controller & UI

Pages: 2


📖 Page 1: Controller

Create PatientController

Create src/main/java/com/healthcare/pms/controller/PatientController.java:

package com.healthcare.pms.controller;

import com.healthcare.pms.dto.PatientDTO;
import com.healthcare.pms.service.PatientService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

@Controller
@RequestMapping("/patients")
@RequiredArgsConstructor
public class PatientController {

    private final PatientService patientService;

    // LIST - GET /patients
    @GetMapping
    public String listPatients(@RequestParam(required = false) String search,
                              @RequestParam(required = false) String gender,
                              Model model) {
        List<PatientDTO> patients;
        if (search != null && !search.isEmpty()) {
            patients = patientService.searchPatients(search);
        } else {
            patients = patientService.filterPatients(gender, null, null);
        }
        model.addAttribute("patients", patients);
        model.addAttribute("search", search);
        model.addAttribute("gender", gender);
        return "patients/list";
    }

    // NEW FORM - GET /patients/new
    @GetMapping("/new")
    public String showCreateForm(Model model) {
        model.addAttribute("patient", new PatientDTO());
        return "patients/form";
    }

    // CREATE - POST /patients
    @PostMapping
    public String createPatient(@Valid @ModelAttribute("patient") PatientDTO dto,
                               BindingResult result, Model model) {
        if (result.hasErrors()) {
            return "patients/form";
        }
        PatientDTO created = patientService.createPatient(dto);
        return "redirect:/patients/" + created.getId();
    }

    // VIEW - GET /patients/{id}
    @GetMapping("/{id}")
    public String viewPatient(@PathVariable String id, Model model) {
        PatientDTO patient = patientService.getPatientById(id);
        model.addAttribute("patient", patient);
        return "patients/view";
    }

    // EDIT FORM - GET /patients/{id}/edit
    @GetMapping("/{id}/edit")
    public String showEditForm(@PathVariable String id, Model model) {
        PatientDTO patient = patientService.getPatientById(id);
        model.addAttribute("patient", patient);
        return "patients/form";
    }

    // UPDATE - POST /patients/{id}
    @PostMapping("/{id}")
    public String updatePatient(@PathVariable String id,
                               @Valid @ModelAttribute("patient") PatientDTO dto,
                               BindingResult result) {
        if (result.hasErrors()) {
            return "patients/form";
        }
        patientService.updatePatient(id, dto);
        return "redirect:/patients/" + id;
    }

    // DELETE - POST /patients/{id}/delete
    @PostMapping("/{id}/delete")
    public String deletePatient(@PathVariable String id) {
        patientService.deletePatient(id);
        return "redirect:/patients";
    }
}

📖 Page 2: Thymeleaf Templates

List Page

Create src/main/resources/templates/patients/list.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Patients</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
    <h1>Patients</h1>

    <!-- Search & Filter -->
    <form class="row mb-3" method="get">
        <div class="col-md-6">
            <input type="text" name="search" class="form-control" 
                   placeholder="Search by name" th:value="${search}">
        </div>
        <div class="col-md-3">
            <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>
            </select>
        </div>
        <div class="col-md-3">
            <button type="submit" class="btn btn-primary">Search</button>
            <a href="/patients/new" class="btn btn-success">+ New</a>
        </div>
    </form>

    <!-- Table -->
    <table class="table table-striped">
        <thead>
            <tr>
                <th>#</th>
                <th>Name</th>
                <th>Age</th>
                <th>Gender</th>
                <th>Email</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
            <tr th:each="patient, stat : ${patients}">
                <td th:text="${stat.count}"></td>
                <td>
                    <a th:href="@{/patients/{id}(id=${patient.id})}" 
                       th:text="${patient.fullName}"></a>
                </td>
                <td th:text="${#temporals.year(#temporals.createNow()) - 
                              #temporals.year(patient.birthDate)}"></td>
                <td th:text="${patient.gender}"></td>
                <td th:text="${patient.email}"></td>
                <td>
                    <a th:href="@{/patients/{id}/edit(id=${patient.id})}" 
                       class="btn btn-sm btn-warning">Edit</a>
                    <form th:action="@{/patients/{id}/delete(id=${patient.id})}" 
                          method="post" style="display:inline">
                        <button class="btn btn-sm btn-danger" 
                                onclick="return confirm('Delete?')">Delete</button>
                    </form>
                </td>
            </tr>
        </tbody>
    </table>
</div>
</body>
</html>

Form Page (Create/Edit)

Create src/main/resources/templates/patients/form.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Patient Form</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
    <h1 th:text="${patient.id == null ? 'New Patient' : 'Edit Patient'}"></h1>

    <form th:object="${patient}" 
          th:action="${patient.id == null ? '/patients' : '/patients/' + patient.id}" 
          method="post">
        <div class="row">
            <div class="col-md-6 mb-3">
                <label>First Name *</label>
                <input type="text" th:field="*{firstName}" class="form-control">
                <span th:if="${#fields.hasErrors('firstName')}" 
                      th:errors="*{firstName}" class="text-danger"></span>
            </div>
            <div class="col-md-6 mb-3">
                <label>Last Name *</label>
                <input type="text" th:field="*{lastName}" class="form-control">
                <span th:if="${#fields.hasErrors('lastName')}" 
                      th:errors="*{lastName}" class="text-danger"></span>
            </div>
        </div>

        <div class="row">
            <div class="col-md-6 mb-3">
                <label>Birth Date *</label>
                <input type="date" th:field="*{birthDate}" class="form-control">
            </div>
            <div class="col-md-6 mb-3">
                <label>Gender *</label>
                <select th:field="*{gender}" class="form-control">
                    <option value="male">Male</option>
                    <option value="female">Female</option>
                    <option value="other">Other</option>
                </select>
            </div>
        </div>

        <div class="row">
            <div class="col-md-6 mb-3">
                <label>Email</label>
                <input type="email" th:field="*{email}" class="form-control">
            </div>
            <div class="col-md-6 mb-3">
                <label>Phone</label>
                <input type="text" th:field="*{phone}" class="form-control">
            </div>
        </div>

        <button type="submit" class="btn btn-primary">Save</button>
        <a href="/patients" class="btn btn-secondary">Cancel</a>
    </form>
</div>
</body>
</html>

View Page

Create src/main/resources/templates/patients/view.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Patient Details</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
    <h1 th:text="${patient.fullName}"></h1>

    <div class="card">
        <div class="card-body">
            <p><strong>ID:</strong> <span th:text="${patient.id}"></span></p>
            <p><strong>Birth Date:</strong> 
               <span th:text="${#temporals.format(patient.birthDate, 'MMM dd, yyyy')}"></span>
            </p>
            <p><strong>Gender:</strong> <span th:text="${patient.gender}"></span></p>
            <p><strong>Email:</strong> <span th:text="${patient.email}"></span></p>
            <p><strong>Phone:</strong> <span th:text="${patient.phone}"></span></p>
            <p><strong>Status:</strong> 
               <span th:text="${patient.active ? 'Active' : 'Inactive'}"></span>
            </p>
        </div>
    </div>

    <div class="mt-3">
        <a th:href="@{/patients/{id}/edit(id=${patient.id})}" 
           class="btn btn-warning">Edit</a>
        <a href="/patients" class="btn btn-secondary">Back to List</a>
    </div>
</div>
</body>
</html>

✅ Web Interface Complete

  • PatientController with all endpoints
  • List page with search/filter
  • Form page (create/edit)
  • View page (details)
  • Bootstrap styling

Test: http://localhost:8081/patients


🚀 Next

Basic CRUD working! Now add advanced filters.

→ Tutorial 6: Search, Filter & Sort