feat(ui): integrate attendance pages with shared shell
This commit is contained in:
@@ -0,0 +1,80 @@
|
|||||||
|
# Test Evidence: attendance shell integration
|
||||||
|
|
||||||
|
- **Test type:** Web
|
||||||
|
- **Requirement IDs:** `UI-001`, `UI-002`, `UI-003`, `UI-008`, `I1-ATT-03`, `I1-UI-04`
|
||||||
|
- **Scenario IDs:** `AC-ATT-003`, `AC-ATT-004`, `AC-UI-001`, `AC-UI-005`
|
||||||
|
- **Test class/method:** `com.lab.labtimesheet.feature.reporting.controller.AttendanceTemplateIntegrationTest`
|
||||||
|
- **Implementation commit:** `pending`
|
||||||
|
|
||||||
|
## Protected behavior
|
||||||
|
|
||||||
|
The Intern attendance-history and Admin global-calendar pages consume the role-aware shared shell while preserving their existing routes, CSRF-protected mutation forms, filter values, empty states, and local theme assets.
|
||||||
|
|
||||||
|
## Test method
|
||||||
|
|
||||||
|
A focused MockMvc slice supplies empty production-shaped models to the two production Attendance templates and renders them with role-specific Spring Security principals. The owning feature's `AttendanceControllerTest` remains the affected behavioral suite for authorization, punch actions, calendar mutation, and view selection.
|
||||||
|
|
||||||
|
## Hand-derived expected result
|
||||||
|
|
||||||
|
Both responses contain `app-shell` and `/assets/theme.js`. Intern history posts to `/attendance/check-in` and `/attendance/check-out` and renders its empty period state. Admin calendar posts to `/attendance/calendar` and renders its empty upcoming-events state. The shell highlights the real attendance/calendar route for the current role.
|
||||||
|
|
||||||
|
## RED
|
||||||
|
|
||||||
|
**Command**
|
||||||
|
|
||||||
|
```text
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||||
|
export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH"
|
||||||
|
./mvnw -Dtest=AttendanceTemplateIntegrationTest test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Observed result**
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tests run: 2, Failures: 2, Errors: 0, Skipped: 0
|
||||||
|
AttendanceTemplateIntegrationTest.adminCalendarUsesSharedShellAndPreservesEventForm expected class="app-shell"
|
||||||
|
AttendanceTemplateIntegrationTest.internHistoryUsesSharedShellAndPreservesPunchActions expected class="app-shell"
|
||||||
|
BUILD FAILURE
|
||||||
|
Total time: 4.977 s
|
||||||
|
```
|
||||||
|
|
||||||
|
Both Attendance templates were standalone HTML documents.
|
||||||
|
|
||||||
|
## GREEN
|
||||||
|
|
||||||
|
**Command**
|
||||||
|
|
||||||
|
```text
|
||||||
|
export PATH="/opt/homebrew/opt/node@24/bin:$PATH"
|
||||||
|
npm run build
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||||
|
export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH"
|
||||||
|
./mvnw -Dtest=AttendanceTemplateIntegrationTest test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Observed result**
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tailwind CSS v4.3.3: Done in 72ms
|
||||||
|
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
BUILD SUCCESS
|
||||||
|
Total time: 3.832 s
|
||||||
|
```
|
||||||
|
|
||||||
|
## Affected suite
|
||||||
|
|
||||||
|
**Command and result**
|
||||||
|
|
||||||
|
```text
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||||
|
export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH"
|
||||||
|
./mvnw -Dtest=AttendanceTemplateIntegrationTest,AttendanceControllerTest test
|
||||||
|
|
||||||
|
Tests run: 9, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
BUILD SUCCESS
|
||||||
|
Total time: 3.974 s
|
||||||
|
```
|
||||||
|
|
||||||
|
## External-test boundaries
|
||||||
|
|
||||||
|
The checks prove server-rendered shell integration and preserve the owning controller's tested contracts. They do not exercise PostgreSQL attendance persistence, live punch timing, browser overflow, or the visual state of populated editable calendar rows; those remain covered by the Attendance feature suite and final integrated UI checks.
|
||||||
@@ -119,6 +119,8 @@
|
|||||||
.form-section legend { padding: 0 .35rem; font-weight: 700; }
|
.form-section legend { padding: 0 .35rem; font-weight: 700; }
|
||||||
.field-help { margin: 0 0 .8rem; color: var(--muted); font-size: .78rem; }
|
.field-help { margin: 0 0 .8rem; color: var(--muted); font-size: .78rem; }
|
||||||
.form-actions { display: flex; justify-content: flex-end; gap: .6rem; }
|
.form-actions { display: flex; justify-content: flex-end; gap: .6rem; }
|
||||||
|
.inline-actions { display: flex; gap: .6rem; margin: 1rem 0; }
|
||||||
|
.filter-form { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; align-items: end; gap: .8rem; margin: 1rem 0; }
|
||||||
.field-label { font-size: .78rem; font-weight: 650; }
|
.field-label { font-size: .78rem; font-weight: 650; }
|
||||||
.control { min-height: 2.45rem; width: 100%; border: 1px solid var(--border-strong); border-radius: .5rem; background: var(--panel); color: var(--ink); padding: .55rem .65rem; }
|
.control { min-height: 2.45rem; width: 100%; border: 1px solid var(--border-strong); border-radius: .5rem; background: var(--panel); color: var(--ink); padding: .55rem .65rem; }
|
||||||
.control[aria-invalid="true"] { border-color: var(--danger); }
|
.control[aria-invalid="true"] { border-color: var(--danger); }
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,42 +1,44 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
<html lang="en" xmlns:th="http://www.thymeleaf.org"
|
||||||
<head>
|
th:replace="~{fragments/layout :: shell(
|
||||||
<meta charset="utf-8">
|
pageTitle='Global calendar',
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
section='Admin',
|
||||||
<title>Global calendar</title>
|
activeNav='calendar',
|
||||||
</head>
|
primaryAction=~{::#primary-action},
|
||||||
|
content=~{::main})}">
|
||||||
<body>
|
<body>
|
||||||
|
<a id="primary-action" class="button" th:href="@{/dashboard}">Back to overview</a>
|
||||||
<main>
|
<main>
|
||||||
<h1>Global calendar</h1>
|
<p class="page-description">Maintain the global events and day-off rules applied to attendance.</p>
|
||||||
<p th:if="${message}" role="status" th:text="${message}"></p>
|
<p class="alert" th:if="${message}" role="status" th:text="${message}"></p>
|
||||||
|
|
||||||
<form method="post" th:action="@{/attendance/calendar}">
|
<form class="panel form-panel form-grid" method="post" th:action="@{/attendance/calendar}">
|
||||||
<label for="date">Date</label>
|
<div class="form-grid form-grid-three">
|
||||||
<input id="date" name="date" type="date" required th:min="${today}">
|
<div class="field"><label class="field-label" for="date">Date</label><input class="control" id="date" name="date" type="date" required th:min="${today}"></div>
|
||||||
<label for="name">Name</label>
|
<div class="field"><label class="field-label" for="name">Name</label><input class="control" id="name" name="name" type="text" maxlength="200" required></div>
|
||||||
<input id="name" name="name" type="text" maxlength="200" required>
|
<label class="checkbox"><input name="dayOff" type="checkbox" value="true"> <span>Day off</span></label>
|
||||||
<label><input name="dayOff" type="checkbox" value="true"> Day off</label>
|
</div>
|
||||||
<button type="submit">Add event</button>
|
<div class="form-actions"><button class="button button-primary" type="submit">Add event</button></div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p th:if="${#lists.isEmpty(events)}">No upcoming calendar events.</p>
|
<section class="panel" th:if="${#lists.isEmpty(events)}"><th:block th:replace="~{fragments/components :: empty('No upcoming calendar events', 'Added events and global days off will appear here.')} "></th:block></section>
|
||||||
<table th:unless="${#lists.isEmpty(events)}">
|
<div class="panel table-scroll" th:unless="${#lists.isEmpty(events)}"><table class="data-table">
|
||||||
<caption>Upcoming global events</caption>
|
<caption>Upcoming global events</caption>
|
||||||
<thead><tr><th scope="col">Date</th><th scope="col">Name</th><th scope="col">Day off</th><th scope="col">Save</th></tr></thead>
|
<thead><tr><th scope="col">Date</th><th scope="col">Name</th><th scope="col">Day off</th><th scope="col">Save</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="event : ${events}">
|
<tr th:each="event : ${events}">
|
||||||
<td><input name="date" type="date" required th:min="${today}" th:value="${event.date}" th:attr="form=|event-${event.id}|" aria-label="Event date"></td>
|
<td><input class="control" name="date" type="date" required th:min="${today}" th:value="${event.date}" th:attr="form=|event-${event.id}|" aria-label="Event date"></td>
|
||||||
<td><input name="name" type="text" maxlength="200" required th:value="${event.name}" th:attr="form=|event-${event.id}|" aria-label="Event name"></td>
|
<td><input class="control" name="name" type="text" maxlength="200" required th:value="${event.name}" th:attr="form=|event-${event.id}|" aria-label="Event name"></td>
|
||||||
<td><input name="dayOff" type="checkbox" value="true" th:checked="${event.dayOff}" th:attr="form=|event-${event.id}|" aria-label="Day off"></td>
|
<td><input name="dayOff" type="checkbox" value="true" th:checked="${event.dayOff}" th:attr="form=|event-${event.id}|" aria-label="Day off"></td>
|
||||||
<td>
|
<td>
|
||||||
<form method="post" th:id="|event-${event.id}|" th:action="@{/attendance/calendar/{id}(id=${event.id})}">
|
<form method="post" th:id="|event-${event.id}|" th:action="@{/attendance/calendar/{id}(id=${event.id})}">
|
||||||
<input name="version" type="hidden" th:value="${event.version}">
|
<input name="version" type="hidden" th:value="${event.version}">
|
||||||
<button type="submit">Save</button>
|
<button class="button" type="submit">Save</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table></div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,33 +1,31 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
<html lang="en" xmlns:th="http://www.thymeleaf.org"
|
||||||
<head>
|
th:replace="~{fragments/layout :: shell(
|
||||||
<meta charset="utf-8">
|
pageTitle=${ownHistory ? 'My attendance' : 'Intern attendance'},
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
section=${ownHistory ? 'Intern' : 'Mentor'},
|
||||||
<title>Attendance history</title>
|
activeNav='attendance',
|
||||||
</head>
|
primaryAction=~{::#primary-action},
|
||||||
|
content=~{::main})}">
|
||||||
<body>
|
<body>
|
||||||
|
<a id="primary-action" class="button" th:href="@{/dashboard}">Back to overview</a>
|
||||||
<main>
|
<main>
|
||||||
<h1 th:text="${ownHistory} ? 'My attendance' : 'Intern attendance'">Attendance</h1>
|
<p class="page-description">Review recorded punches and the policy snapshot applied to each work date.</p>
|
||||||
<p th:if="${message}" role="status" th:text="${message}"></p>
|
<p class="alert" th:if="${message}" role="status" th:text="${message}"></p>
|
||||||
<p th:if="${error}" role="alert" th:text="${error}"></p>
|
<p class="alert alert-error" th:if="${error}" role="alert" th:text="${error}"></p>
|
||||||
|
|
||||||
<form th:if="${ownHistory}" method="post" th:action="@{/attendance/check-in}">
|
<div class="inline-actions" th:if="${ownHistory}">
|
||||||
<button type="submit">Check in</button>
|
<form method="post" th:action="@{/attendance/check-in}"><button class="button button-primary" type="submit">Check in</button></form>
|
||||||
</form>
|
<form method="post" th:action="@{/attendance/check-out}"><button class="button" type="submit">Check out</button></form>
|
||||||
<form th:if="${ownHistory}" method="post" th:action="@{/attendance/check-out}">
|
</div>
|
||||||
<button type="submit">Check out</button>
|
|
||||||
|
<form class="panel form-panel filter-form" method="get">
|
||||||
|
<div class="field"><label class="field-label" for="from">From</label><input class="control" id="from" name="from" type="date" th:value="${from}"></div>
|
||||||
|
<div class="field"><label class="field-label" for="to">To</label><input class="control" id="to" name="to" type="date" th:value="${to}"></div>
|
||||||
|
<button class="button" type="submit">Filter</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form method="get">
|
<section class="panel" th:if="${#lists.isEmpty(items)}"><th:block th:replace="~{fragments/components :: empty('No attendance records in this period', 'Choose another date range or record your first punch.')} "></th:block></section>
|
||||||
<label for="from">From</label>
|
<div class="panel table-scroll" th:unless="${#lists.isEmpty(items)}"><table class="data-table">
|
||||||
<input id="from" name="from" type="date" th:value="${from}">
|
|
||||||
<label for="to">To</label>
|
|
||||||
<input id="to" name="to" type="date" th:value="${to}">
|
|
||||||
<button type="submit">Filter</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p th:if="${#lists.isEmpty(items)}">No attendance records in this period.</p>
|
|
||||||
<table th:unless="${#lists.isEmpty(items)}">
|
|
||||||
<caption>Attendance records and applied policy</caption>
|
<caption>Attendance records and applied policy</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -49,7 +47,7 @@
|
|||||||
<td th:text="${item.violations.missingCheckout ? 'Missing checkout' : (item.violations.earlyDeparture ? 'Early departure' : (item.violations.late ? 'Late' : 'On time'))}"></td>
|
<td th:text="${item.violations.missingCheckout ? 'Missing checkout' : (item.violations.earlyDeparture ? 'Early departure' : (item.violations.late ? 'Late' : 'On time'))}"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table></div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -22,10 +22,10 @@
|
|||||||
<li><a class="nav-link" th:href="@{/dashboard}" th:attr="aria-current=${activeNav == 'dashboard'} ? 'page' : null">
|
<li><a class="nav-link" th:href="@{/dashboard}" th:attr="aria-current=${activeNav == 'dashboard'} ? 'page' : null">
|
||||||
<svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#layout-dashboard}"></use></svg><span class="sidebar-label">Overview</span></a></li>
|
<svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#layout-dashboard}"></use></svg><span class="sidebar-label">Overview</span></a></li>
|
||||||
<li sec:authorize="hasRole('ADMIN')"><a class="nav-link" th:href="@{/admin/accounts/new}" th:attr="aria-current=${activeNav == 'accounts'} ? 'page' : null"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#users}"></use></svg><span class="sidebar-label">Accounts</span></a></li>
|
<li sec:authorize="hasRole('ADMIN')"><a class="nav-link" th:href="@{/admin/accounts/new}" th:attr="aria-current=${activeNav == 'accounts'} ? 'page' : null"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#users}"></use></svg><span class="sidebar-label">Accounts</span></a></li>
|
||||||
<li sec:authorize="hasRole('ADMIN')"><a class="nav-link" th:href="@{/admin/calendar}"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#calendar-days}"></use></svg><span class="sidebar-label">Global calendar</span></a></li>
|
<li sec:authorize="hasRole('ADMIN')"><a class="nav-link" th:href="@{/attendance/calendar}" th:attr="aria-current=${activeNav == 'calendar'} ? 'page' : null"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#calendar-days}"></use></svg><span class="sidebar-label">Global calendar</span></a></li>
|
||||||
<li sec:authorize="hasRole('MENTOR')"><a class="nav-link" th:href="@{/projects}"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#folder-kanban}"></use></svg><span class="sidebar-label">Owned Projects</span></a></li>
|
<li sec:authorize="hasRole('MENTOR')"><a class="nav-link" th:href="@{/projects}"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#folder-kanban}"></use></svg><span class="sidebar-label">Owned Projects</span></a></li>
|
||||||
<li sec:authorize="hasRole('MENTOR')"><a class="nav-link" th:href="@{/attendance}"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#clock}"></use></svg><span class="sidebar-label">Intern attendance</span></a></li>
|
<li sec:authorize="hasRole('MENTOR')"><a class="nav-link" th:href="@{/attendance}" th:attr="aria-current=${activeNav == 'attendance'} ? 'page' : null"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#clock}"></use></svg><span class="sidebar-label">Intern attendance</span></a></li>
|
||||||
<li sec:authorize="hasRole('INTERN')"><a class="nav-link" th:href="@{/attendance/me}"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#clock}"></use></svg><span class="sidebar-label">My attendance</span></a></li>
|
<li sec:authorize="hasRole('INTERN')"><a class="nav-link" th:href="@{/attendance/me}" th:attr="aria-current=${activeNav == 'attendance'} ? 'page' : null"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#clock}"></use></svg><span class="sidebar-label">My attendance</span></a></li>
|
||||||
<li sec:authorize="hasRole('INTERN')"><a class="nav-link" th:href="@{/projects}"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#folder-kanban}"></use></svg><span class="sidebar-label">My Projects</span></a></li>
|
<li sec:authorize="hasRole('INTERN')"><a class="nav-link" th:href="@{/projects}"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#folder-kanban}"></use></svg><span class="sidebar-label">My Projects</span></a></li>
|
||||||
<li sec:authorize="hasRole('INTERN')"><a class="nav-link" th:href="@{/tasks}"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#list-check}"></use></svg><span class="sidebar-label">Assigned Tasks</span></a></li>
|
<li sec:authorize="hasRole('INTERN')"><a class="nav-link" th:href="@{/tasks}"><svg class="nav-icon" aria-hidden="true"><use th:href="@{/assets/icons.svg#list-check}"></use></svg><span class="sidebar-label">Assigned Tasks</span></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
package com.lab.labtimesheet.feature.reporting.controller;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
@WebMvcTest(AttendanceTemplateIntegrationTest.TemplateController.class)
|
||||||
|
@Import(AttendanceTemplateIntegrationTest.TemplateController.class)
|
||||||
|
class AttendanceTemplateIntegrationTest {
|
||||||
|
|
||||||
|
private final MockMvc mvc;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
AttendanceTemplateIntegrationTest(MockMvc mvc) {
|
||||||
|
this.mvc = mvc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void internHistoryUsesSharedShellAndPreservesPunchActions() throws Exception {
|
||||||
|
mvc.perform(get("/template-contract/attendance/history")
|
||||||
|
.with(user("intern@example.test").roles("INTERN")))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().string(containsString("class=\"app-shell\"")))
|
||||||
|
.andExpect(content().string(containsString("action=\"/attendance/check-in\"")))
|
||||||
|
.andExpect(content().string(containsString("action=\"/attendance/check-out\"")))
|
||||||
|
.andExpect(content().string(containsString("No attendance records in this period")))
|
||||||
|
.andExpect(content().string(containsString("src=\"/assets/theme.js\"")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void adminCalendarUsesSharedShellAndPreservesEventForm() throws Exception {
|
||||||
|
mvc.perform(get("/template-contract/attendance/calendar")
|
||||||
|
.with(user("admin@example.test").roles("ADMIN")))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().string(containsString("class=\"app-shell\"")))
|
||||||
|
.andExpect(content().string(containsString("action=\"/attendance/calendar\"")))
|
||||||
|
.andExpect(content().string(containsString("No upcoming calendar events")))
|
||||||
|
.andExpect(content().string(containsString("src=\"/assets/theme.js\"")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public static class TemplateController {
|
||||||
|
|
||||||
|
@GetMapping("/template-contract/attendance/history")
|
||||||
|
String history(Model model) {
|
||||||
|
model.addAttribute("ownHistory", true);
|
||||||
|
model.addAttribute("from", LocalDate.of(2026, 8, 1));
|
||||||
|
model.addAttribute("to", LocalDate.of(2026, 8, 31));
|
||||||
|
model.addAttribute("items", List.of());
|
||||||
|
return "attendance/history";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/template-contract/attendance/calendar")
|
||||||
|
String calendar(Model model) {
|
||||||
|
model.addAttribute("today", LocalDate.of(2026, 8, 15));
|
||||||
|
model.addAttribute("events", List.of());
|
||||||
|
return "attendance/calendar";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user