← Back to Portfolio

Resource & Booking Management System

Full-Stack Enterprise Application Showcase

Java 21 Spring Boot 3 Thymeleaf Bootstrap 5

Backend Architecture (Spring Boot 3)

This project implements a robust REST and MVC architecture using JPA/Hibernate for data persistence, Jakarta Validation for security, and Spring Services for transactional business logic.

📄 pom.xml (Maven Configuration)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>
                    
📄 application.properties

# Server Port
server.port=8080

# Database Configuration (H2 In-Memory)
spring.datasource.url=jdbc:h2:mem:bookingdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password

# Hibernate properties
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
                    
📄 Booking.java (JPA Entity)

@Entity
@Table(name = "bookings")
public class Booking {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull(message = "Resource must be selected")
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "resource_id")
    private Resource resource;
    
    @NotNull(message = "Client must be assigned")
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "client_id")
    private Client client;

    @Future(message = "Start date must be in the future")
    @NotNull(message = "Start date is required")
    private LocalDateTime startTime;

    @Column(nullable = false)
    private String status = "PENDING";
}
                    
📄 Client.java (JPA Entity)

@Entity
@Table(name = "clients")
public class Client {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank(message = "Company/Client name is required")
    private String name;

    @Email(message = "Invalid email format")
    @NotBlank(message = "Email is required")
    private String email;

    @OneToMany(mappedBy = "client", cascade = CascadeType.ALL)
    private List<Booking> bookings = new ArrayList<>();
}
                    
📄 BookingRepository.java (Spring Data JPA)

package com.booking.repository;

import com.booking.model.Booking;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;

@Repository
public interface BookingRepository extends JpaRepository<Booking, Long> {
    List<Booking> findByStatus(String status);
    List<Booking> findByResourceIdAndStartTimeAfter(
            Long resourceId, LocalDateTime time);
}
                    
📄 BookingService.java (Transaction Layer)

@Service
public class BookingService {

    private final BookingRepository repository;

    public BookingService(BookingRepository repository) {
        this.repository = repository;
    }

    @Transactional
    public Booking createBooking(Booking booking) {
        if (!isResourceAvailable(booking.getResource(), 
                                 booking.getStartTime())) {
            throw new ResourceConflictException("Resource taken!");
        }
        return repository.save(booking);
    }
}
                    
📄 WebController.java (Spring MVC)

@Controller
public class WebController {

    private final BookingService bookingService;

    public WebController(BookingService bookingService) {
        this.bookingService = bookingService;
    }

    @GetMapping("/dashboard")
    public String showDashboard(Model model) {
        // Zasilenie modelu listą z bazy danych
        model.addAttribute("bookings", bookingService.findAllBookings());
        // Wyrenderowanie widoku
        return "dashboard";
    }
}
                    
📄 dashboard.html (Thymeleaf - Snippet)

<tbody>
    <tr th:each="booking : ${bookings}">
        <td class="text-muted" th:text="'#BK-' + ${booking.id}">#BK-101</td>
        <td class="fw-bold" th:text="${booking.resource.name}">Resource Name</td>
        <td th:text="${booking.client.name}">Client Name</td>
        <td th:text="${#temporals.format(booking.startTime, 'yyyy-MM-dd HH:mm')}">Date</td>
        <td>
            <span class="badge" 
                  th:classappend="${booking.status == 'CONFIRMED' ? 'bg-success' : 'bg-warning text-dark'}"
                  th:text="${booking.status}">STATUS</span>
        </td>
        <td><button class="btn btn-sm btn-outline-secondary">Edit</button></td>
    </tr>
</tbody>
                    

Frontend Live Preview (Thymeleaf + Bootstrap 5)

Below is a static rendering of the Thymeleaf dashboard template. It utilizes responsive Bootstrap 5 utility classes, metric cards, and a data table, styled to match the dark Camel Linen aesthetic.

localhost:8080/dashboard
Total Bookings

1,284

Active Resources

42 / 50

Pending Approvals

7

Recent Bookings
ID Resource Client Date Status Action
#BK-101 Conference Room A TechCorp Inc. 2026-08-10 10:00 CONFIRMED
#BK-102 Projector 4K Startup LLC 2026-08-11 14:00 PENDING
#BK-103 Server Rack 04 Internal IT 2026-08-09 08:00 CANCELLED