done dashboard

This commit is contained in:
2026-07-02 16:06:26 +07:00
parent 2181547184
commit 986bd08e5f
6 changed files with 344 additions and 126 deletions
+1 -2
View File
@@ -14,7 +14,6 @@ Tags Tạo và quản lý các Thẻ (nhãn từ khóa tìm kiếm nhanh).
Add New Tải tệp tin mới lên máy chủ.
(Done) 4. Pages (Trang) All Pages Quản lý các trang tĩnh (ví dụ: Trang chủ, Trang liên hệ, Giới thiệu bệnh viện, Giờ làm việc...).
Add New Tạo trang tĩnh mới.
5. Comments (Bình luận) (Không có) Quản lý bình luận của người đọc (Phê duyệt, Trả lời, Đánh dấu Spam, Xóa tạm/Xóa vĩnh viễn).
6. Appearance (Giao diện) Themes Cài đặt, kích hoạt hoặc xóa các giao diện hiển thị của website.
Customize Chỉnh sửa giao diện trực quan theo thời gian thực (Header, Footer, Colors, Font, v.v.).
Widgets Quản lý các khối nội dung ở Sidebar hoặc Footer.
@@ -30,7 +29,7 @@ Profile Chỉnh sửa thông tin cá nhân của tài khoản đang đăng nhậ
Import / Export Nhập/Xuất toàn bộ bài viết, trang, media sang file .xml để sao lưu hoặc chuyển host.
Site Health Kiểm tra sức khỏe hệ thống (phiên bản PHP, MySQL, các lỗi bảo mật).
Export / Erase Personal Data Xuất hoặc xóa dữ liệu cá nhân của người dùng để tuân thủ luật bảo mật (GDPR).
10. Settings (Cài đặt) General Thiết lập tên website, câu khẩu hiệu, địa chỉ email admin, múi giờ, định dạng ngày tháng.
(done) 10. Settings (Cài đặt) General Thiết lập tên website, câu khẩu hiệu, địa chỉ email admin, múi giờ, định dạng ngày tháng.
Writing Cấu hình mặc định khi soạn thảo văn bản và gửi bài viết qua email.
Reading Thiết lập trang chủ hiển thị, số lượng bài viết tối đa trên một trang blog, chặn công cụ tìm kiếm.
Discussion Cấu hình bình luận (yêu cầu phê duyệt, chặn từ ngữ nhạy cảm).
@@ -1,11 +1,25 @@
package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.domain.Post;
import com.sisvietnamvn.web.domain.PageStatus;
import com.sisvietnamvn.web.repository.PostRepository;
import com.sisvietnamvn.web.repository.PageRepository;
import com.sisvietnamvn.web.service.PostService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.security.access.prepost.PreAuthorize;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import java.io.File;
import java.lang.management.ManagementFactory;
import com.sun.management.OperatingSystemMXBean;
import java.util.List;
import java.util.stream.Collectors;
/**
* Controller for the admin dashboard index page.
* Serves the main "/manage" landing page.
@@ -15,8 +29,69 @@ import com.sisvietnamvn.web.security.AuthoritiesConstants;
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\", \"" + AuthoritiesConstants.AUTHOR + "\", \"" + AuthoritiesConstants.CONTRIBUTOR + "\", \"" + AuthoritiesConstants.SUBSCRIBER + "\", \"" + AuthoritiesConstants.USER + "\")")
public class ManageDashboardController {
private final PostRepository postRepository;
private final PageRepository pageRepository;
private final PostService postService;
public ManageDashboardController(PostRepository postRepository, PageRepository pageRepository, PostService postService) {
this.postRepository = postRepository;
this.pageRepository = pageRepository;
this.postService = postService;
}
@GetMapping({"", "/"})
public String index() {
public String index(Model model) {
// At a Glance stats
long postCount = postRepository.count();
long pageCount = pageRepository.count();
model.addAttribute("postCount", postCount);
model.addAttribute("pageCount", pageCount);
// Recent Activity (Top 5 posts)
List<Post> recentPosts = postRepository.findAllByOrderByCreatedDateDesc().stream()
.limit(5)
.collect(Collectors.toList());
model.addAttribute("recentPosts", recentPosts);
// Telemetry Stats
Runtime runtime = Runtime.getRuntime();
long totalRam = runtime.totalMemory();
long freeRam = runtime.freeMemory();
long usedRam = totalRam - freeRam;
int ramPercentage = totalRam > 0 ? (int) ((usedRam * 100.0f) / totalRam) : 0;
model.addAttribute("ramPercentage", ramPercentage);
model.addAttribute("usedRamMb", usedRam / (1024 * 1024));
model.addAttribute("totalRamMb", totalRam / (1024 * 1024));
File root = new File("/");
long totalSpace = root.getTotalSpace();
long freeSpace = root.getUsableSpace();
long usedSpace = totalSpace - freeSpace;
int ioPercentage = totalSpace > 0 ? (int) ((usedSpace * 100.0f) / totalSpace) : 0;
model.addAttribute("ioPercentage", ioPercentage);
model.addAttribute("usedSpaceGb", usedSpace / (1024 * 1024 * 1024));
model.addAttribute("totalSpaceGb", totalSpace / (1024 * 1024 * 1024));
try {
OperatingSystemMXBean osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);
double cpuLoad = osBean.getCpuLoad(); // returns 0.0 to 1.0. Returns < 0 if not available.
int cpuPercentage = cpuLoad >= 0 ? (int) (cpuLoad * 100) : 0;
model.addAttribute("cpuPercentage", cpuPercentage);
} catch (Exception e) {
model.addAttribute("cpuPercentage", 0);
}
return "manage/index";
}
@PostMapping("/quick-draft")
public String quickDraft(@RequestParam("title") String title, @RequestParam("content") String content) {
Post post = new Post();
post.setTitle(title);
post.setContent(content);
post.setStatus(PageStatus.DRAFT);
postService.save(post);
return "redirect:/manage";
}
}
@@ -0,0 +1,62 @@
package com.sisvietnamvn.web.controller.manage;
import com.sisvietnamvn.web.service.SettingService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import java.util.Map;
/**
* Controller for the admin Updates page.
*/
@Controller
@RequestMapping("/manage/updates")
@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\")")
public class ManageUpdatesController {
private final SettingService settingService;
private static final String LATEST_VERSION = "4.0.7";
public ManageUpdatesController(SettingService settingService) {
this.settingService = settingService;
}
@GetMapping
public String updates(Model model) {
Map<String, String> settings = settingService.getAllAsMap();
String currentVersion = settings.getOrDefault("core_version", "4.0.6");
boolean updateAvailable = !LATEST_VERSION.equals(currentVersion);
model.addAttribute("coreVersionCurrent", currentVersion);
model.addAttribute("coreVersionLatest", LATEST_VERSION);
model.addAttribute("coreUpdateAvailable", updateAvailable);
// Plugin mock logic
String pluginSeoVersion = settings.getOrDefault("plugin_seo_version", "1.0.0");
model.addAttribute("pluginSeoVersion", pluginSeoVersion);
model.addAttribute("pluginSeoUpdateAvailable", !"1.1.2".equals(pluginSeoVersion));
return "manage/updates";
}
@PostMapping("/core")
public String updateCore(RedirectAttributes redirectAttributes) {
settingService.setValue("core_version", LATEST_VERSION);
redirectAttributes.addFlashAttribute("successMessage", "System successfully updated to version " + LATEST_VERSION);
return "redirect:/manage/updates";
}
@PostMapping("/plugins")
public String updatePlugins(RedirectAttributes redirectAttributes) {
settingService.setValue("plugin_seo_version", "1.1.2");
redirectAttributes.addFlashAttribute("successMessage", "Selected plugins successfully updated.");
return "redirect:/manage/updates";
}
}
@@ -40,11 +40,19 @@
<!-- Divider -->
<hr class="sidebar-divider my-0">
<!-- Nav Item - Dashboard -->
<!-- Nav Item - Dashboard Collapse Menu -->
<li class="nav-item active">
<a class="nav-link" th:href="@{/manage}">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#collapseDashboard"
aria-expanded="true" aria-controls="collapseDashboard">
<i class="fas fa-fw fa-tachometer-alt"></i>
<span>Dashboard</span></a>
<span>Dashboard</span>
</a>
<div id="collapseDashboard" class="collapse" aria-labelledby="headingDashboard" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<a class="collapse-item" th:href="@{/manage}">Home</a>
<a class="collapse-item" th:href="@{/manage/updates}">Updates</a>
</div>
</div>
</li>
<!-- Divider -->
@@ -11,145 +11,110 @@
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">Dashboard</h1>
<a href="#" class="d-none d-sm-inline-block btn btn-sm btn-primary shadow-sm"><i
class="fas fa-download fa-sm text-white-50"></i> Generate Report</a>
</div>
<!-- Content Row -->
<div class="row">
<!-- Earnings (Monthly) Card Example -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
Earnings (Monthly)</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">$40,000</div>
</div>
<div class="col-auto">
<i class="fas fa-calendar fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Earnings (Annual) Card Example -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
Earnings (Annual)</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">$215,000</div>
</div>
<div class="col-auto">
<i class="fas fa-dollar-sign fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Tasks Card Example -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">Tasks
</div>
<div class="row no-gutters align-items-center">
<div class="col-auto">
<div class="h5 mb-0 mr-3 font-weight-bold text-gray-800">50%</div>
</div>
<div class="col">
<div class="progress progress-sm mr-2">
<div class="progress-bar bg-info" role="progressbar"
style="width: 50%" aria-valuenow="50" aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div>
</div>
</div>
<div class="col-auto">
<i class="fas fa-clipboard-list fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Pending Requests Card Example -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
Pending Requests</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">18</div>
</div>
<div class="col-auto">
<i class="fas fa-comments fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Content Row -->
<div class="row">
<!-- Content Column -->
<div class="col-lg-12 mb-4">
<!-- Project Card Example -->
<!-- Server Health -->
<div class="col-lg-6 mb-4">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Projects</h6>
<h6 class="m-0 font-weight-bold text-primary">Site Health & Telemetry</h6>
</div>
<div class="card-body">
<h4 class="small font-weight-bold">Server Migration <span
class="float-right">20%</span></h4>
<h4 class="small font-weight-bold">CPU Load <span
class="float-right" th:text="${cpuPercentage} + '%'">0%</span></h4>
<div class="progress mb-4">
<div class="progress-bar bg-danger" role="progressbar" style="width: 20%"
aria-valuenow="20" aria-valuemin="0" aria-valuemax="100"></div>
<div th:class="'progress-bar ' + (${cpuPercentage} > 80 ? 'bg-danger' : (${cpuPercentage} > 50 ? 'bg-warning' : 'bg-success'))"
role="progressbar" th:style="'width: ' + ${cpuPercentage} + '%'"
th:aria-valuenow="${cpuPercentage}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<h4 class="small font-weight-bold">Sales Tracking <span
class="float-right">40%</span></h4>
<h4 class="small font-weight-bold">Memory (RAM) <span
class="float-right" th:text="${usedRamMb} + ' MB / ' + ${totalRamMb} + ' MB (' + ${ramPercentage} + '%)'">0 MB / 0 MB</span></h4>
<div class="progress mb-4">
<div class="progress-bar bg-warning" role="progressbar" style="width: 40%"
aria-valuenow="40" aria-valuemin="0" aria-valuemax="100"></div>
<div th:class="'progress-bar ' + (${ramPercentage} > 80 ? 'bg-danger' : (${ramPercentage} > 50 ? 'bg-warning' : 'bg-info'))"
role="progressbar" th:style="'width: ' + ${ramPercentage} + '%'"
th:aria-valuenow="${ramPercentage}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<h4 class="small font-weight-bold">Customer Database <span
class="float-right">60%</span></h4>
<h4 class="small font-weight-bold">Disk Space (I/O) <span
class="float-right" th:text="${usedSpaceGb} + ' GB / ' + ${totalSpaceGb} + ' GB (' + ${ioPercentage} + '%)'">0 GB / 0 GB</span></h4>
<div class="progress mb-4">
<div class="progress-bar" role="progressbar" style="width: 60%"
aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<h4 class="small font-weight-bold">Payout Details <span
class="float-right">80%</span></h4>
<div class="progress mb-4">
<div class="progress-bar bg-info" role="progressbar" style="width: 80%"
aria-valuenow="80" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<h4 class="small font-weight-bold">Account Setup <span
class="float-right">Complete!</span></h4>
<div class="progress">
<div class="progress-bar bg-success" role="progressbar" style="width: 100%"
aria-valuenow="100" aria-valuemin="0" aria-valuemax="100"></div>
<div th:class="'progress-bar ' + (${ioPercentage} > 80 ? 'bg-danger' : (${ioPercentage} > 50 ? 'bg-warning' : 'bg-primary'))"
role="progressbar" th:style="'width: ' + ${ioPercentage} + '%'"
th:aria-valuenow="${ioPercentage}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</div>
<!-- Quick Draft -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Quick Draft</h6>
</div>
<div class="card-body">
<form th:action="@{/manage/quick-draft}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<div class="form-group">
<input type="text" class="form-control" name="title" placeholder="Title" required>
</div>
<div class="form-group">
<textarea class="form-control" name="content" rows="4" placeholder="What's on your mind?"></textarea>
</div>
<button type="submit" class="btn btn-outline-primary btn-sm">Save Draft</button>
</form>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<!-- At a Glance -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">At a Glance</h6>
</div>
<div class="card-body">
<div class="row text-center mb-3">
<div class="col-6">
<i class="fas fa-file-alt fa-2x text-gray-300 mb-2"></i>
<h4 class="h5 text-gray-800"><span th:text="${postCount}">0</span> Posts</h4>
</div>
<div class="col-6">
<i class="fas fa-file fa-2x text-gray-300 mb-2"></i>
<h4 class="h5 text-gray-800"><span th:text="${pageCount}">0</span> Pages</h4>
</div>
</div>
<p class="text-muted text-center mb-0 small">Running Spring Boot 4.0.6 (Java 21)</p>
</div>
</div>
<!-- Recent Activity -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Recent Activity</h6>
</div>
<div class="card-body">
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center" th:each="post : ${recentPosts}">
<div>
<span class="text-muted small mr-2" th:text="${#temporals.format(post.createdDate, 'dd/MM/yyyy HH:mm')}">Date</span>
<a th:href="@{/manage/posts/{id}/edit(id=${post.id})}" th:text="${post.title}">Post Title</a>
</div>
<span class="badge badge-pill"
th:classappend="${post.status == 'PUBLISHED' ? 'badge-success' : (post.status == 'DRAFT' ? 'badge-secondary' : 'badge-warning')}"
th:text="${post.status}">DRAFT</span>
</li>
<li class="list-group-item text-muted text-center small" th:if="${#lists.isEmpty(recentPosts)}">
No recent activity
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,109 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
<head>
<title>System Updates</title>
</head>
<body>
<div layout:fragment="content">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800">System Updates</h1>
</div>
<!-- Success Message -->
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert">
<span th:text="${successMessage}"></span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="row">
<!-- Core Update -->
<div class="col-lg-12 mb-4">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary" th:text="${coreUpdateAvailable} ? 'An updated version of the system is available.' : 'You have the latest version of the system.'">An updated version of the system is available.</h6>
</div>
<div class="card-body">
<p><strong>Important:</strong> Before updating, please back up your database and files. For help with updates, visit the <a href="#">Updating Documentation</a>.</p>
<div th:if="${coreUpdateAvailable}" class="alert alert-warning">
<strong>Update Available:</strong> You are currently running version <span th:text="${coreVersionCurrent}">X</span>.
Version <span th:text="${coreVersionLatest}">Y</span> is available.
</div>
<div th:unless="${coreUpdateAvailable}" class="alert alert-success">
<strong>Up to Date:</strong> You are currently running version <span th:text="${coreVersionCurrent}">X</span>. No core updates are available.
</div>
<form th:action="@{/manage/updates/core}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<button type="submit" class="btn btn-primary" th:disabled="${!coreUpdateAvailable}">Update Now</button>
</form>
<p class="text-muted small mt-2">While your site is being updated, it will be in maintenance mode. As soon as your updates are complete, your site will return to normal.</p>
</div>
</div>
</div>
<!-- Plugins Update -->
<div class="col-lg-6 mb-4">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Plugins</h6>
</div>
<div class="card-body">
<p th:if="${pluginSeoUpdateAvailable}">The following plugins have new versions available. Check the ones you want to update and then click "Update Plugins".</p>
<p th:unless="${pluginSeoUpdateAvailable}">Your plugins are all up to date.</p>
<form th:action="@{/manage/updates/plugins}" method="post" th:if="${pluginSeoUpdateAvailable}">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<table class="table table-bordered">
<thead>
<tr>
<th><input type="checkbox" checked disabled></th>
<th>Plugin Name</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="checkbox" checked disabled></td>
<td><strong>Sample SEO Plugin</strong><br><small>You have version <span th:text="${pluginSeoVersion}">1.0.0</span>. Update to 1.1.2.</small></td>
</tr>
</tbody>
</table>
<button type="submit" class="btn btn-secondary btn-sm">Update Selected Plugins</button>
</form>
</div>
</div>
</div>
<!-- Themes & Translations Update -->
<div class="col-lg-6 mb-4">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Themes</h6>
</div>
<div class="card-body">
<p>Your themes are all up to date.</p>
</div>
</div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Translations</h6>
</div>
<div class="card-body">
<p>Your translations are all up to date.</p>
</div>
</div>
</div>
</div>
</div>
</body>
</html>