feat(reporting): add role dashboard template contracts
This commit is contained in:
@@ -0,0 +1,82 @@
|
|||||||
|
# Test Evidence: role dashboard template contract
|
||||||
|
|
||||||
|
- **Test type:** Web
|
||||||
|
- **Requirement IDs:** `AUTH-003`, `UI-003`, `UI-013`, `I1-UI-03`
|
||||||
|
- **Scenario IDs:** `AC-AUTH-002`, `AC-UI-005`
|
||||||
|
- **Test class/method:** `com.lab.labtimesheet.feature.reporting.ReportingArchitectureTest`, `com.lab.labtimesheet.feature.reporting.controller.DashboardTemplateWebTest`
|
||||||
|
- **Implementation commit:** `pending`
|
||||||
|
|
||||||
|
## Protected behavior
|
||||||
|
|
||||||
|
Reporting-owned Java starts under `com.lab.labtimesheet.feature.reporting` rather than global layer packages or a placeholder module marker. The Admin, Mentor, and Intern dashboard templates consume typed view DTOs and render role-correct metrics, actions, attendance state, and `dd/MM/yyyy` dates without illustrative production data.
|
||||||
|
|
||||||
|
## Test method
|
||||||
|
|
||||||
|
The architecture test loads the reporting view contract and rejects the superseded global-layer and module-boundary classes. A narrow MVC test controller supplies explicit DTO fixtures to the production Thymeleaf templates so their rendering contract can be verified before cross-feature service APIs are integrated. It does not replace the later database-backed `/dashboard` test.
|
||||||
|
|
||||||
|
## Hand-derived expected result
|
||||||
|
|
||||||
|
Admin markup contains system metrics and `Create account`; Mentor markup contains owned Project and global pending-decision summaries plus `Create Project`; Intern markup contains only the supplied assigned Task, checked-in state, `Check out`, and due date `18/08/2026`. Actions belonging to other roles are absent.
|
||||||
|
|
||||||
|
## RED
|
||||||
|
|
||||||
|
**Command**
|
||||||
|
|
||||||
|
```text
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||||
|
export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH"
|
||||||
|
./mvnw clean -Dtest=ReportingArchitectureTest test
|
||||||
|
./mvnw clean -Dtest=DashboardTemplateWebTest test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Observed result**
|
||||||
|
|
||||||
|
```text
|
||||||
|
ReportingArchitectureTest: ClassNotFoundException: com.lab.labtimesheet.feature.reporting.model.dto.DashboardView
|
||||||
|
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0
|
||||||
|
|
||||||
|
DashboardTemplateWebTest: Error resolving template [dashboard/admin], [dashboard/mentor], and [dashboard/intern]
|
||||||
|
Tests run: 3, Failures: 0, Errors: 3, Skipped: 0
|
||||||
|
BUILD FAILURE
|
||||||
|
```
|
||||||
|
|
||||||
|
The final feature package DTO and the three production dashboard templates were absent in the respective pre-implementation states.
|
||||||
|
|
||||||
|
## GREEN
|
||||||
|
|
||||||
|
**Command**
|
||||||
|
|
||||||
|
```text
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||||
|
export PATH="/opt/homebrew/opt/node@24/bin:$JAVA_HOME/bin:$PATH"
|
||||||
|
./mvnw clean -Dtest=ReportingArchitectureTest,DashboardTemplateWebTest test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Observed result**
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
BUILD SUCCESS
|
||||||
|
Total time: 25.380 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"
|
||||||
|
npm run build
|
||||||
|
./mvnw -Dtest=UiContractWebTest,ReportingArchitectureTest,DashboardTemplateWebTest test
|
||||||
|
|
||||||
|
v24.19.0 / npm 11.17.0
|
||||||
|
Tailwind CSS v4.3.3: Done in 56ms
|
||||||
|
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
BUILD SUCCESS
|
||||||
|
Total time: 24.823 s
|
||||||
|
```
|
||||||
|
|
||||||
|
## External-test boundaries
|
||||||
|
|
||||||
|
These tests prove package and template contracts with controlled view DTOs. They deliberately do not claim that `/dashboard` is connected to real account, Project, Task, attendance, or notification data; that integration remains gated on the platform's cross-feature service APIs and will require a PostgreSQL/Testcontainers test after the pinned platform structure is merged. Browser viewport, contrast, and pre-paint behavior remain integrated UI gates.
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
- **Requirement IDs:** `ARC-004`, `UI-001`–`UI-010`, `UI-013`–`UI-018`, `I1-UI-01`, `I1-UI-02`, `I1-UI-04`
|
- **Requirement IDs:** `ARC-004`, `UI-001`–`UI-010`, `UI-013`–`UI-018`, `I1-UI-01`, `I1-UI-02`, `I1-UI-04`
|
||||||
- **Scenario IDs:** `AC-UI-001`, `AC-UI-002`, `AC-UI-003`, `AC-UI-005`
|
- **Scenario IDs:** `AC-UI-001`, `AC-UI-002`, `AC-UI-003`, `AC-UI-005`
|
||||||
- **Test class/method:** `com.lab.labtimesheet.ui.UiContractWebTest`
|
- **Test class/method:** `com.lab.labtimesheet.ui.UiContractWebTest`
|
||||||
- **Implementation commit:** `pending`
|
- **Implementation commit:** `bac3981`
|
||||||
|
|
||||||
## Protected behavior
|
## Protected behavior
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.lab.labtimesheet.feature.reporting.model.dto;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public sealed interface DashboardView {
|
||||||
|
|
||||||
|
record Admin(long activeAccounts, long pendingActivations,
|
||||||
|
long activeInternships, long activeProjects) implements DashboardView {
|
||||||
|
}
|
||||||
|
|
||||||
|
record Mentor(String displayName, long activeProjects, long activeMembers,
|
||||||
|
long blockedTasks, long pendingDecisions) implements DashboardView {
|
||||||
|
}
|
||||||
|
|
||||||
|
record Intern(String displayName, AttendanceState attendanceState, long activeProjects,
|
||||||
|
long assignedTasks, long unreadNotifications,
|
||||||
|
List<AssignedTask> priorityTasks) implements DashboardView {
|
||||||
|
}
|
||||||
|
|
||||||
|
record AssignedTask(String title, String projectName, String status, LocalDate dueDate) {
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AttendanceState {
|
||||||
|
NOT_CHECKED_IN("Not checked in"),
|
||||||
|
CHECKED_IN("Checked in"),
|
||||||
|
CHECKED_OUT("Checked out");
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
AttendanceState(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String label() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.lab.labtimesheet.reporting;
|
|
||||||
|
|
||||||
/** Reporting module boundary. */
|
|
||||||
public final class ModuleBoundary {
|
|
||||||
private ModuleBoundary() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,26 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{fragments/layout :: shell(
|
||||||
|
pageTitle='System overview',
|
||||||
|
section='Admin',
|
||||||
|
activeNav='dashboard',
|
||||||
|
primaryAction=~{::#primary-action},
|
||||||
|
content=~{::main})}">
|
||||||
|
<body>
|
||||||
|
<a id="primary-action" class="button button-primary" th:href="@{/admin/users/new}">Create account</a>
|
||||||
|
<main>
|
||||||
|
<p class="page-description">Account readiness, internship activity, and active Project work.</p>
|
||||||
|
<section class="panel metric-strip" aria-label="System metrics">
|
||||||
|
<div class="metric"><div class="metric-label">Active accounts</div><div class="metric-value" th:text="${dashboard.activeAccounts}">0</div><div class="metric-detail">Can authenticate when otherwise eligible</div></div>
|
||||||
|
<div class="metric"><div class="metric-label">Pending activation</div><div class="metric-value" th:text="${dashboard.pendingActivations}">0</div><div class="metric-detail">Awaiting first password</div></div>
|
||||||
|
<div class="metric"><div class="metric-label">Active internships</div><div class="metric-value" th:text="${dashboard.activeInternships}">0</div><div class="metric-detail">Current Intern lifecycle</div></div>
|
||||||
|
<div class="metric"><div class="metric-label">Active Projects</div><div class="metric-value" th:text="${dashboard.activeProjects}">0</div><div class="metric-detail">Read-only Admin scope</div></div>
|
||||||
|
</section>
|
||||||
|
<section class="panel" th:if="${dashboard.pendingActivations == 0}"><th:block th:replace="~{fragments/components :: empty('No pending activations', 'New accounts awaiting activation will appear here.')} "></th:block></section>
|
||||||
|
<section class="panel" th:unless="${dashboard.pendingActivations == 0}">
|
||||||
|
<header class="panel-header"><h2 class="panel-title">Attention required</h2></header>
|
||||||
|
<div class="alert alert-warning" role="status"><strong th:text="${dashboard.pendingActivations}">0</strong> account(s) are waiting for activation.</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{fragments/layout :: shell(
|
||||||
|
pageTitle='Today',
|
||||||
|
section='Intern',
|
||||||
|
activeNav='dashboard',
|
||||||
|
primaryAction=~{::#primary-action},
|
||||||
|
content=~{::main})}">
|
||||||
|
<body>
|
||||||
|
<form id="primary-action" th:if="${dashboard.attendanceState.name() != 'CHECKED_OUT'}"
|
||||||
|
th:action="${dashboard.attendanceState.name() == 'CHECKED_IN' ? '/attendance/check-out' : '/attendance/check-in'}" method="post">
|
||||||
|
<button class="button button-primary" type="submit" th:text="${dashboard.attendanceState.name() == 'CHECKED_IN' ? 'Check out' : 'Check in'}">Check in</button>
|
||||||
|
</form>
|
||||||
|
<main>
|
||||||
|
<p class="page-description" th:text="${'Attendance, assigned work, and Project activity for ' + dashboard.displayName + '.'}">Today.</p>
|
||||||
|
<section class="panel metric-strip" aria-label="Intern metrics">
|
||||||
|
<div class="metric"><div class="metric-label">Attendance</div><div class="metric-value" th:text="${dashboard.attendanceState.label}">Not checked in</div><div class="metric-detail">Server-authoritative state</div></div>
|
||||||
|
<div class="metric"><div class="metric-label">Active Projects</div><div class="metric-value" th:text="${dashboard.activeProjects}">0</div><div class="metric-detail">Current memberships</div></div>
|
||||||
|
<div class="metric"><div class="metric-label">Assigned Tasks</div><div class="metric-value" th:text="${dashboard.assignedTasks}">0</div><div class="metric-detail">Current assignment scope</div></div>
|
||||||
|
<div class="metric"><div class="metric-label">Unread notifications</div><div class="metric-value" th:text="${dashboard.unreadNotifications}">0</div><div class="metric-detail">Own notifications only</div></div>
|
||||||
|
</section>
|
||||||
|
<section class="panel">
|
||||||
|
<header class="panel-header"><h2 class="panel-title">Priority Tasks</h2></header>
|
||||||
|
<th:block th:if="${#lists.isEmpty(dashboard.priorityTasks)}"><th:block th:replace="~{fragments/components :: empty('No assigned Tasks', 'Assigned active-Project Tasks will appear here.')} "></th:block></th:block>
|
||||||
|
<div class="table-scroll" th:unless="${#lists.isEmpty(dashboard.priorityTasks)}">
|
||||||
|
<table class="data-table">
|
||||||
|
<caption class="sr-only">Current assigned Tasks</caption>
|
||||||
|
<thead><tr><th scope="col">Task</th><th scope="col">Project</th><th scope="col">Status</th><th scope="col">Due</th></tr></thead>
|
||||||
|
<tbody><tr th:each="task : ${dashboard.priorityTasks}"><td th:text="${task.title}">Task</td><td th:text="${task.projectName}">Project</td><td><span class="badge" th:classappend="${task.status == 'BLOCKED' ? ' badge-warning' : task.status == 'DONE' ? ' badge-success' : ''}" th:attr="aria-label=${'Status: ' + task.status}" th:text="${task.status}">TODO</span></td><td th:text="${task.dueDate == null ? 'No due date' : #temporals.format(task.dueDate, 'dd/MM/yyyy')}">No due date</td></tr></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{fragments/layout :: shell(
|
||||||
|
pageTitle=${'Good morning, ' + dashboard.displayName},
|
||||||
|
section='Mentor',
|
||||||
|
activeNav='dashboard',
|
||||||
|
primaryAction=~{::#primary-action},
|
||||||
|
content=~{::main})}">
|
||||||
|
<body>
|
||||||
|
<a id="primary-action" class="button button-primary" th:href="@{/projects/new}">Create Project</a>
|
||||||
|
<main>
|
||||||
|
<p class="page-description">Review pending decisions, then scan attendance and owned Project health.</p>
|
||||||
|
<section class="panel metric-strip" aria-label="Mentor metrics">
|
||||||
|
<div class="metric"><div class="metric-label">Active owned Projects</div><div class="metric-value" th:text="${dashboard.activeProjects}">0</div><div class="metric-detail">Only your Project scope</div></div>
|
||||||
|
<div class="metric"><div class="metric-label">Active members</div><div class="metric-value" th:text="${dashboard.activeMembers}">0</div><div class="metric-detail">Across owned active Projects</div></div>
|
||||||
|
<div class="metric"><div class="metric-label">Blocked Tasks</div><div class="metric-value" th:text="${dashboard.blockedTasks}">0</div><div class="metric-detail">View and comment</div></div>
|
||||||
|
<div class="metric"><div class="metric-label">Pending decisions</div><div class="metric-value" th:text="${dashboard.pendingDecisions}">0</div><div class="metric-detail">Global leave and corrections</div></div>
|
||||||
|
</section>
|
||||||
|
<section class="panel" th:if="${dashboard.pendingDecisions == 0}"><th:block th:replace="~{fragments/components :: empty('No pending decisions', 'New leave and correction requests will appear in their queues.')} "></th:block></section>
|
||||||
|
<section class="panel" th:unless="${dashboard.pendingDecisions == 0}"><header class="panel-header"><h2 class="panel-title">Decision queue</h2></header><div class="alert" role="status"><strong th:text="${dashboard.pendingDecisions}">0</strong> request(s) need Mentor review.</div></section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.lab.labtimesheet.feature.reporting;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class ReportingArchitectureTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reportingUsesFeaturePackageWithoutGlobalLayersOrPlaceholderBoundary() throws Exception {
|
||||||
|
Class<?> view = Class.forName("com.lab.labtimesheet.feature.reporting.model.dto.DashboardView");
|
||||||
|
assertTrue(view.isSealed());
|
||||||
|
assertFalse(view.getPackageName().startsWith("com.lab.labtimesheet.model"));
|
||||||
|
|
||||||
|
assertMissing("com.lab.labtimesheet.controller.DashboardController");
|
||||||
|
assertMissing("com.lab.labtimesheet.dto.DashboardView");
|
||||||
|
assertMissing("com.lab.labtimesheet.exception.DashboardAccessDeniedException");
|
||||||
|
assertMissing("com.lab.labtimesheet.model.DashboardAccount");
|
||||||
|
assertMissing("com.lab.labtimesheet.repository.DashboardRepository");
|
||||||
|
assertMissing("com.lab.labtimesheet.service.DashboardService");
|
||||||
|
assertMissing("com.lab.labtimesheet.reporting.ModuleBoundary");
|
||||||
|
assertMissing("com.lab.labtimesheet.feature.reporting.ModuleBoundary");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertMissing(String className) {
|
||||||
|
assertThrows(ClassNotFoundException.class, () -> Class.forName(className));
|
||||||
|
}
|
||||||
|
}
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
package com.lab.labtimesheet.feature.reporting.controller;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
|
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 com.lab.labtimesheet.feature.reporting.model.dto.DashboardView;
|
||||||
|
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.security.test.context.support.WithMockUser;
|
||||||
|
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(DashboardTemplateWebTest.TemplateController.class)
|
||||||
|
@Import(DashboardTemplateWebTest.TemplateController.class)
|
||||||
|
class DashboardTemplateWebTest {
|
||||||
|
|
||||||
|
private final MockMvc mvc;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
DashboardTemplateWebTest(MockMvc mvc) {
|
||||||
|
this.mvc = mvc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(username = "admin@example.test", roles = "ADMIN")
|
||||||
|
void adminTemplateRendersSystemMetricsAndOnlyAdminAction() throws Exception {
|
||||||
|
mvc.perform(get("/template-contract/dashboard/admin"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().string(containsString("System overview")))
|
||||||
|
.andExpect(content().string(containsString("Active accounts</div><div class=\"metric-value\">3")))
|
||||||
|
.andExpect(content().string(containsString("Create account")))
|
||||||
|
.andExpect(content().string(not(containsString("Create Project"))))
|
||||||
|
.andExpect(content().string(not(containsString("Check in"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(username = "mentor@example.test", roles = "MENTOR")
|
||||||
|
void mentorTemplateRendersOwnedScopeAndOnlyMentorAction() throws Exception {
|
||||||
|
mvc.perform(get("/template-contract/dashboard/mentor"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().string(containsString("Good morning, Minh Mentor")))
|
||||||
|
.andExpect(content().string(containsString("Active owned Projects</div><div class=\"metric-value\">1")))
|
||||||
|
.andExpect(content().string(containsString("Pending decisions</div><div class=\"metric-value\">2")))
|
||||||
|
.andExpect(content().string(containsString("Create Project")))
|
||||||
|
.andExpect(content().string(not(containsString("Create account"))))
|
||||||
|
.andExpect(content().string(not(containsString("Check in"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(username = "intern@example.test", roles = "INTERN")
|
||||||
|
void internTemplateRendersOwnWorkAndAttendanceAction() throws Exception {
|
||||||
|
mvc.perform(get("/template-contract/dashboard/intern"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().string(containsString("Today")))
|
||||||
|
.andExpect(content().string(containsString("Checked in")))
|
||||||
|
.andExpect(content().string(containsString("My real task")))
|
||||||
|
.andExpect(content().string(containsString("18/08/2026")))
|
||||||
|
.andExpect(content().string(containsString("Check out")))
|
||||||
|
.andExpect(content().string(not(containsString("Create Project"))))
|
||||||
|
.andExpect(content().string(not(containsString("Create account"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public static class TemplateController {
|
||||||
|
|
||||||
|
@GetMapping("/template-contract/dashboard/admin")
|
||||||
|
String admin(Model model) {
|
||||||
|
model.addAttribute("dashboard", new DashboardView.Admin(3, 1, 2, 1));
|
||||||
|
return "dashboard/admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/template-contract/dashboard/mentor")
|
||||||
|
String mentor(Model model) {
|
||||||
|
model.addAttribute("dashboard", new DashboardView.Mentor("Minh Mentor", 1, 2, 1, 2));
|
||||||
|
return "dashboard/mentor";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/template-contract/dashboard/intern")
|
||||||
|
String intern(Model model) {
|
||||||
|
model.addAttribute("dashboard", new DashboardView.Intern(
|
||||||
|
"Mai Intern",
|
||||||
|
DashboardView.AttendanceState.CHECKED_IN,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
List.of(new DashboardView.AssignedTask(
|
||||||
|
"My real task", "Intern Portal", "IN_PROGRESS", LocalDate.of(2026, 8, 18)))));
|
||||||
|
return "dashboard/intern";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user