feat: enhance menu management with three-level hierarchy support, additional metadata fields, and utility scripts for data extraction

This commit is contained in:
2026-07-22 10:30:42 +07:00
parent f37be85873
commit abdf9467e6
21 changed files with 859 additions and 2348 deletions
+13
View File
@@ -29,3 +29,16 @@ Use the following exact structure as a template:
</body> </body>
</html> </html>
``` ```
### Dynamic Theme Component Inclusion
When writing or modifying Thymeleaf templates for the public frontend, NEVER hardcode paths for common layout components (such as header or footer) to a specific theme (e.g., `themes/umass/header`) or to the default fallback (e.g., `fragments/footer`) unless explicitly building an admin/manage page.
Instead, ALWAYS use the `activeTheme` variable to dynamically load the active theme's component.
**Correct Usage:**
`<header th:replace="~{themes/__${activeTheme}__/header :: header}"></header>`
`<footer th:replace="~{themes/__${activeTheme}__/footer :: footer}"></footer>`
**Incorrect Usage:**
`<div th:replace="~{themes/umass/header}"></div>`
`<footer th:replace="~{fragments/footer :: footer}"></footer>`
@@ -0,0 +1,168 @@
{
"info": {
"name": "SIS Lịch Đặt Khám API (Fixed)",
"description": "Collection tự động tạo từ tài liệu hướng dẫn API lịch đặt khám của bệnh viện S.I.S Cần Thơ.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "1. Get Token",
"event": [
{
"listen": "test",
"script": {
"exec": [
"var jsonData = pm.response.json();",
"pm.globals.set(\"access_token\", jsonData.access_token);"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/x-www-form-urlencoded"
},
{
"key": "Cookie",
"value": ".AspNetCore.Culture=c%3Den%7Cuic%3Den; __tenant=3a0f747c-57b6-753b-ba21-3987e8e93b9a"
}
],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "grant_type",
"value": "client_credentials",
"type": "text"
},
{
"key": "client_id",
"value": "DigitalHealthSolutions_LabConn",
"type": "text"
},
{
"key": "client_secret",
"value": "AcEnBCaphERGOGYMaStATITEnaPETYpR",
"type": "text"
},
{
"key": "scope",
"value": "LabConn",
"type": "text"
}
]
},
"url": {
"raw": "{{base_url}}/connect/token",
"host": [
"{{base_url}}"
],
"path": [
"connect",
"token"
]
},
"description": "Lấy Access Token để chứng thực cho các API khác."
}
},
{
"name": "2. Danh sách bác sĩ có lịch làm việc",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{access_token}}",
"type": "string"
}
]
},
"method": "GET",
"header": [
{
"key": "__tenant",
"value": "3a0f747c-57b6-753b-ba21-3987e8e93b9a"
},
{
"key": "Cookie",
"value": ".AspNetCore.Culture=c%3Den%7Cuic%3Den; __tenant=3a0f747c-57b6-753b-ba21-3987e8e93b9a"
}
],
"url": {
"raw": "{{base_url}}/api/app/his/doctors/work-schedules",
"host": [
"{{base_url}}"
],
"path": [
"api",
"app",
"his",
"doctors",
"work-schedules"
]
},
"description": "Lấy danh sách các bác sĩ đang có lịch làm việc."
}
},
{
"name": "3. Lịch làm việc chi tiết bác sĩ",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{access_token}}",
"type": "string"
}
]
},
"method": "GET",
"header": [
{
"key": "__tenant",
"value": "3a0f747c-57b6-753b-ba21-3987e8e93b9a"
},
{
"key": "Cookie",
"value": ".AspNetCore.Culture=c%3Den%7Cuic%3Den; __tenant=3a0f747c-57b6-753b-ba21-3987e8e93b9a"
}
],
"url": {
"raw": "{{base_url}}/api/app/his/doctors/:doctorCode/work-schedule-days",
"host": [
"{{base_url}}"
],
"path": [
"api",
"app",
"his",
"doctors",
":doctorCode",
"work-schedule-days"
],
"variable": [
{
"key": "doctorCode",
"value": "00492",
"description": "Mã bác sĩ (ví dụ 00492)"
}
]
},
"description": "Lấy chi tiết các ngày làm việc của một bác sĩ thông qua mã bác sĩ (DoctorCode)."
}
}
],
"variable": [
{
"key": "base_url",
"value": "https://dhs.sisvietnam.vn",
"type": "string"
}
]
}
+259
View File
@@ -0,0 +1,259 @@
{
"info": {
"_postman_id": "4c495f9e-cbb0-4bcc-9721-2c5cf9120247",
"name": "Web",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "6738901"
},
"item": [
{
"name": "get token",
"event": [
{
"listen": "test",
"script": {
"exec": [
"var jsonData = pm.response.json();\r",
"pm.globals.set(\"access_token\", jsonData.access_token);"
],
"type": "text/javascript",
"packages": {},
"requests": {}
}
},
{
"listen": "prerequest",
"script": {
"exec": [
""
],
"type": "text/javascript",
"packages": {},
"requests": {}
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/x-www-form-urlencoded"
},
{
"key": "Cookie",
"value": ".AspNetCore.Culture=c%3Den%7Cuic%3Den; __tenant=3a0f747c-57b6-753b-ba21-3987e8e93b9a"
}
],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"type": "text",
"key": "grant_type",
"value": "client_credentials"
},
{
"type": "text",
"key": "client_id",
"value": "DigitalHealthSolutions_LabConn"
},
{
"type": "text",
"key": "client_secret",
"value": "AcEnBCaphERGOGYMaStATITEnaPETYpR"
},
{
"type": "text",
"key": "scope",
"value": "LabConn"
}
]
},
"url": {
"raw": "{{url}}/connect/token",
"host": [
"{{url}}"
],
"path": [
"connect",
"token"
]
}
},
"response": []
},
{
"name": "lich lam viec chi tiet bs",
"event": [
{
"listen": "test",
"script": {
"exec": [
""
],
"type": "text/javascript",
"packages": {},
"requests": {}
}
}
],
"protocolProfileBehavior": {
"disableBodyPruning": true
},
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdGNjFGNTMzMTAyRUI4OTlFQTc2Mjk3MDQ4MUVFQTQ2QTkxODdBMTYiLCJ4NXQiOiJmMkgxTXhBdXVKbnFkaWx3U0I3cVJxa1llaFkiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2Rocy5zaXN2aWV0bmFtLnZuLyIsImV4cCI6MTc4NDY5MTk1MSwiaWF0IjoxNzg0NjA1NTUxLCJhdWQiOiJEaWdpdGFsSGVhbHRoU29sdXRpb25zIiwic2NvcGUiOiJMYWJDb25uIiwianRpIjoiNWJkNTlhY2ItZjM5Yy00NGZhLTgyNmMtYTM4NjFkNDNmZGIxIiwib2lfcHJzdCI6IkRpZ2l0YWxIZWFsdGhTb2x1dGlvbnNfTGFiQ29ubiIsImNsaWVudF9pZCI6IkRpZ2l0YWxIZWFsdGhTb2x1dGlvbnNfTGFiQ29ubiIsIm9pX3Rrbl9pZCI6IjNhMjI5NGY0LTg0NGItMjJkZC05ZDNjLTQ4MjU2MmMyNjQxMCJ9.XwlnWLRrvuzr43CZjtOy6x9uIQqQZscjptTojX_ZlKA7uAuin6hn6ftgXyE8salvDmrETuS-1iWBiIkPaG2HAxjnf1rDtjw0MCfZBcqJC8Q1FOcskQhZn7GUUirVtEvv26t7LTHyCfjApwNtHnHmsfeIhYPbMb8A4PUikMABqd6qwgk8MLSA25endvloNmPUdLmov7xrPJIlePtYJorf0O-bi87WTMANDvMg6xRIY2CTPuDlzRTnsA5N4nDKcUzYHc75gwkC7U_5FLMhPulHB4ep8eEzUlU9TpageslzidSytBKwxGL_xM5Ts3UrPWKWt8dm5qCmw0B2Lnl0AvNttw",
"type": "string"
}
]
},
"method": "GET",
"header": [
{
"key": "__tenant",
"value": "3a0f747c-57b6-753b-ba21-3987e8e93b9a"
},
{
"key": "Cookie",
"value": ".AspNetCore.Culture=c%3Den%7Cuic%3Den; __tenant=3a0f747c-57b6-753b-ba21-3987e8e93b9a"
},
{
"key": "Authorization",
"value": "Brear",
"type": "text",
"disabled": true
}
],
"body": {
"mode": "raw",
"raw": "",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{url}}/api/app/his/doctors/:doctorCode/work-schedule-days",
"host": [
"{{url}}"
],
"path": [
"api",
"app",
"his",
"doctors",
":doctorCode",
"work-schedule-days"
],
"variable": [
{
"key": "doctorCode",
"value": "00492"
}
]
}
},
"response": []
},
{
"name": "get ds ich làm bs",
"event": [
{
"listen": "test",
"script": {
"exec": [
""
],
"type": "text/javascript",
"packages": {},
"requests": {}
}
}
],
"protocolProfileBehavior": {
"disableBodyPruning": true
},
"request": {
"auth": {
"type": "noauth"
},
"method": "GET",
"header": [
{
"key": "__tenant",
"value": "3a0f747c-57b6-753b-ba21-3987e8e93b9a"
},
{
"key": "Cookie",
"value": ".AspNetCore.Culture=c%3Den%7Cuic%3Den; __tenant=3a0f747c-57b6-753b-ba21-3987e8e93b9a",
"disabled": true
},
{
"key": "Authorization",
"value": "Bearer {{access_token}}",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{url}}/api/app/his/doctors/work-schedules",
"host": [
"{{url}}"
],
"path": [
"api",
"app",
"his",
"doctors",
"work-schedules"
]
}
},
"response": []
}
],
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"packages": {},
"requests": {},
"exec": [
""
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"packages": {},
"requests": {},
"exec": [
""
]
}
}
],
"variable": [
{
"key": "url",
"value": ""
}
]
}
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
[]
+15
View File
@@ -0,0 +1,15 @@
import sys
from pypdf import PdfReader
pdf_path = sys.argv[1]
output_path = sys.argv[2]
reader = PdfReader(pdf_path)
text = ""
for i, page in enumerate(reader.pages):
text += f"\n--- Page {i + 1} ---\n"
text += page.extract_text()
with open(output_path, "w", encoding="utf-8") as f:
f.write(text)
print(f"Extracted {len(reader.pages)} pages to {output_path}")
+68
View File
@@ -0,0 +1,68 @@
--- Page 1 ---
Api l ị ch đ ặ t khám - web
1. Get Token
Link:
Ph ươ ng th ứ c: POST
../connect/token
Header
Body x-www-form-urlencoded
Response
https://dhs.sisvietnam.vn
__tenant 3a0f747c-57b6-753b-ba21-
3987e8e93b9a
Content-Type application/json
Key Values
grant_type x client_credentials
client_id x DigitalHealthSolutions_LabConn
client_secret x AcEnBCaphERGOGYMaStATITEnaP
ETYpR
scope x LabConn
Thu ộ c tính B ắ t bu ộ c value
1{2 "access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjdGNjFGNTMzMTAyRUI4OTlFQTc2Mjk3MDQ4MUVFQTQ2QTkxODdBMTYiLCJ4NXQiOiJmMkgxTXhBdXVKbnFkaWx3U0I3cVJxa1llaFkiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2Rocy5zaXN2aWV0bmFtLnZuLyIsImV4cCI6MTc4NDY5MjA1OCwiaWF0IjoxNzg0NjA1NjU4LCJhdWQiOiJEaWdpdGFsSGVhbHRoU29sdXRpb25zIiwic2NvcGUiOiJMYWJDb25uIiwianRpIjoiOWVlZDNhNzItZTcxZC00MWM4LWJhYTgtMzVlMTY5YmEwMzE5Iiwib2lfcHJzdCI6IkRpZ2l0YWxIZWFsdGhTb2x1dGlvbnNfTGFiQ29ubiIsImNsaWVudF9pZCI6IkRpZ2l0YWxIZWFsdGhTb2x1dGlvbnNfTGFiQ29ubiIsIm9pX3Rrbl9pZCI6IjNhMjI5NGY2LTI2NjMtNTQ0OC1iMjU4LWI5Yjk0YzYxYWQ2ZSJ9.R7GQQxWU7MqKnQMhaZPtIMAxwjkTklmPgoFF8aisAZTPq4z7TnMAL36tlKvLeazIKfE0KIVtdfi9AqXeIwXt6co4_-5i9MJ03QJvJVdqul9nGiXaRtuSUagPogclkZ9wZeZU4IHxSFS7WCKuuX-Rv4EjNP2H-Igr5ss5cqfw7mIgQLknIlQ7jQolxqeO9IJgDBH90lxnpaC0S8ZTg3S_6cv2kPG8s8FdqgodOJnFG-d7YFlS4WR9O_z0uRnBS9cIhCRaxoo1GqtzvLVPjeyJecy48DnrNsqYEwsNQx4hWTiHC_9rFlfKCIuWQnv7wKWkkNt-ZsUABkk5PZrtHeZVeg",
--- Page 2 ---
2. Danh sách bác s ĩ có l ị ch làm vi ệ c
Ph ươ ng th ứ c: GET
…/api/app/his/doctors/work-schedules
Header
Response
3 "token_type":"Bearer",4 "expires_in":863985}
__tenant 3a0f747c-57b6-753b-ba21-3987e8e93b9a
Authorization Bearer + Token
Key Vaules
1{2 "data":{3 "fromDate":"2026-07-15",4 "toDate":"2026-07-29",5 "doctors":[6 {7 "doctorId":"feaf54ac-e8c2-42a5-bb3b-65e9ba7fc2a7",8 "doctorCode":"00001",9 "fullName":"TS. BS. TR Ầ N CHÍ C Ư Ờ NG",10 "birthYear":null,11 "gender":null,12 "practicingCertificate":"001257/BYT-CCHN",13 "title":"TS. BS"14 }15 ]16 },17 "success":true,18 "statusCode":200,19 "timestamp":"2026-07-15T10:14:59.0784+07:00"20}
fromDate date Làm vi ệ c t ừ ngày
toDate date Làm vi ệ c đ ế n ngày
doctorId GUID Id bác s ĩ
doctorCode string Mã bác s ĩ
fullName string Tên bác s ĩ
birthYear string Gi ớ i tính
N ộ i dung Ki ể u d ữ li ệ u Mô t ả
--- Page 3 ---
3. L ị ch làm vi ệ c chi ti ế t bác s ĩ
Ph ươ ng th ứ c: GET
…/api/app/his/doctors/{DoctorCode}/work-schedule-days
Header
Response
gender string N ă m sinh
practicingCertificat
e
string Ch ứ ng ch ỉ hành ngh ề
title string Ch ứ c danh
__tenant 3a0f747c-57b6-753b-ba21-3987e8e93b9a
Authorization Bearer + Token
Key Vaules
1{2 "data":{3 "doctorId":"feaf54ac-e8c2-42a5-bb3b-65e9ba7fc2a7",4 "doctorCode":"00001",5 "fromDate":"2026-07-15",6 "toDate":"2026-07-29",7 "days":[8 {9 "date":"2026-07-15",10 "dayOfWeek":411 },12 {13 "date":"2026-07-20",14 "dayOfWeek":215 },16 {17 "date":"2026-07-27",18 "dayOfWeek":219 },20 {21 "date":"2026-07-29",22 "dayOfWeek":423 }24 ]25 },26 "success":true,27 "statusCode":200,28 "timestamp":"2026-07-15T08:59:26.2920087+07:00"29}
N ộ i dung Ki ể u d ữ li ệ uMô t ả
--- Page 4 ---
doctorId GUID ID bác s ĩ
doctorCode string Mã bác s ĩ
fromDate string Làm vi ệ c t ừ ngày
toDate string Làm vi ệ c đ ế n ngày
date string Ngày làm vi ệ c
dayOfWeek int Th ứ :
1: th ứ 2
2: th ứ 3
3: th ứ 4
4: th ứ 5
5: th ứ 6
File diff suppressed because it is too large Load Diff
@@ -1,53 +1,66 @@
<div layout:fragment="content-top"> <div class="submenu-wrapper" style="left: -671.344px;width: 1920px;">
<!-- Content Top (Banner 3 lớp kết hợp Title bài Post) --> <button class="button-back">
<div class="content-top"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 10" width="5" height="10">
<div class="lc--layout-container lc--full"> <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#881c1c" points="0,5 5,0 5,10 "></polygon>
<div class="l--layout l--full"> </svg> Back </button>
<div class="lr--layout-region lr--main"> <span class="navigation-title">VỀ BỆNH VIỆN</span>
<div class="cc--component-container cc--homepage-hero">
<div class="c--component c--homepage-hero"
style="position: relative; overflow: hidden; height: 100vh; max-height: 38.125rem;">
<!-- ====== ẢNH NỀN (Dùng ảnh bài viết hoặc ảnh mặc định) ====== --> <!-- Mobile Submenu Layout (Simple List) -->
<img class="static-fallback-image" th:src="${post.featuredImage != null and !post.featuredImage.isEmpty() ? post.featuredImage : 'https://res.cloudinary.com/ipufdhda/image/upload/v1784534008/BANNER-WEB-scaled_yeox4u.jpg'}" th:alt="${post.title}" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; z-index: 1;" /> <ul class="submenu mobile-only">
<li class="menu-item">
<a href="#">GIỚI THIỆU TỔNG QUAN</a>
</li>
<li class="menu-item">
<a href="#">LỊCH SỬ BỆNH VIỆN</a>
</li>
<li class="menu-item">
<a href="#">TẦM NHÌN - SỨ MỆNH - GIÁ TRỊ CỐT LÕI</a>
</li>
<li class="menu-item">
<a href="#">BAN LÃNH ĐẠO</a>
</li>
<li class="menu-item">
<a href="#">CÁC ĐƠN VỊ</a>
</li>
</ul>
<!-- ====== TEXT OVERLAY (Nội dung chữ nằm trên ảnh, Hiển thị Tiêu đề bài viết) ====== --> <!-- Mega Menu Content Layout -->
<!-- Thêm một lớp nền mờ (rgba đen) để chữ luôn hiển thị rõ ràng trên mọi hình ảnh --> <div class="mega-menu-content desktop-only">
<div class="text-container centered" <!-- Column 1: Info (Title and Text) -->
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 4; pointer-events: none; display: flex; align-items: center; justify-content: center; background-color: rgba(0, 0, 0, 0.4);"> <div class="mega-col-info">
<div class="slide-text-container-inner" style="pointer-events: auto; text-align: center; padding: 20px;"> <h3>VỀ BỆNH VIỆN</h3>
<div class="f--field f--eyebrow" th:if="${post.category != null}" th:text="${post.category.name}" style="color: white; text-shadow: 0 2px 5px rgba(0,0,0,0.7); margin-bottom: 10px; font-weight: bold; text-transform: uppercase;"> <p style="text-transform: math-auto;">Bệnh viện Đột quỵ Quốc tế tự hào là đơn vị tiên phong trong việc cung cấp dịch vụ khám chữa bệnh chất lượng cao, mang lại sự tin cậy và an tâm cho bệnh nhân.</p>
University News
</div>
<div class="f--field f--page-title">
<h1 class="hero-post-title" th:text="${post.title}">Post Title</h1>
</div>
</div>
</div>
<style>
.hero-post-title { </div>
color: white;
text-shadow: 0 2px 5px rgba(0,0,0,0.7); <!-- Column 2: The actual Submenu Links -->
line-height: 1.2; <div class="mega-col-links">
margin: 0 auto; <ul class="submenu">
max-width: 1000px; <li class="menu-item">
font-size: 3.5rem; <a href="#">GIỚI THIỆU TỔNG QUAN</a>
} </li>
@media (max-width: 1024px) { <li class="menu-item">
.hero-post-title { font-size: 2.8rem; max-width: 800px; } <a href="#">LỊCH SỬ BỆNH VIỆN</a>
} </li>
@media (max-width: 768px) { <li class="menu-item">
.hero-post-title { font-size: 2.2rem; max-width: 600px; } <a href="#">TẦM NHÌN - SỨ MỆNH - GIÁ TRỊ CỐT LÕI</a>
} </li>
@media (max-width: 480px) { <li class="menu-item">
.hero-post-title { font-size: 1.8rem; } <a href="#">BAN LÃNH ĐẠO</a>
} </li>
</style> <li class="menu-item">
</div> <a href="#">CÁC ĐƠN VỊ</a>
</div> </li>
</div> </ul>
</div> </div>
<!-- Column 3: Featured Image -->
<div class="mega-col-image">
<!-- Try to use a hospital-like image if we can, or fallback to logo -->
<img src="/theme-assets/umass/images/logo.png" alt="Featured Image" style="object-fit: contain; background: white; padding: 20px;">
</div>
</div> </div>
</div> <!-- End Mega Menu Layout -->
</div>
</div>
Binary file not shown.
@@ -51,11 +51,13 @@ public class ManageMenuController {
} }
@PostMapping("/{id}/items/add") @PostMapping("/{id}/items/add")
public String addMenuItem(@PathVariable Long id, @RequestParam String label, @RequestParam String url, @RequestParam(required = false) Long parentId, RedirectAttributes redirectAttributes) { public String addMenuItem(@PathVariable Long id, @RequestParam String label, @RequestParam String url, @RequestParam(required = false) Long parentId, @RequestParam(required = false) String title, @RequestParam(required = false) String imageUrl, RedirectAttributes redirectAttributes) {
Menu menu = menuService.findById(id).orElseThrow(() -> new IllegalArgumentException("Invalid menu Id")); Menu menu = menuService.findById(id).orElseThrow(() -> new IllegalArgumentException("Invalid menu Id"));
MenuItem item = new MenuItem(); MenuItem item = new MenuItem();
item.setLabel(label); item.setLabel(label);
item.setUrl(url); item.setUrl(url);
item.setTitle(title);
item.setImageUrl(imageUrl);
item.setMenu(menu); item.setMenu(menu);
item.setDisplayOrder(menu.getItems().size()); item.setDisplayOrder(menu.getItems().size());
@@ -92,10 +94,12 @@ public class ManageMenuController {
} }
@PostMapping("/{id}/items/{itemId}/update") @PostMapping("/{id}/items/{itemId}/update")
public String updateMenuItem(@PathVariable Long id, @PathVariable Long itemId, @RequestParam String label, @RequestParam String url, @RequestParam(required = false) Long parentId, @RequestParam(required = false) Integer displayOrder, RedirectAttributes redirectAttributes) { public String updateMenuItem(@PathVariable Long id, @PathVariable Long itemId, @RequestParam String label, @RequestParam String url, @RequestParam(required = false) Long parentId, @RequestParam(required = false) Integer displayOrder, @RequestParam(required = false) String title, @RequestParam(required = false) String imageUrl, RedirectAttributes redirectAttributes) {
MenuItem item = menuItemRepository.findById(itemId).orElseThrow(() -> new IllegalArgumentException("Invalid item Id")); MenuItem item = menuItemRepository.findById(itemId).orElseThrow(() -> new IllegalArgumentException("Invalid item Id"));
item.setLabel(label); item.setLabel(label);
item.setUrl(url); item.setUrl(url);
item.setTitle(title);
item.setImageUrl(imageUrl);
if (displayOrder != null) { if (displayOrder != null) {
item.setDisplayOrder(displayOrder); item.setDisplayOrder(displayOrder);
} }
@@ -43,6 +43,14 @@ public class MenuItem extends AbstractAuditingEntity<Long> {
@OrderBy("displayOrder ASC") @OrderBy("displayOrder ASC")
private java.util.Set<MenuItem> children = new java.util.LinkedHashSet<>(); private java.util.Set<MenuItem> children = new java.util.LinkedHashSet<>();
@Size(max = 255)
@Column(name = "title", length = 255)
private String title;
@Size(max = 500)
@Column(name = "image_url", length = 500)
private String imageUrl;
@Column(name = "display_order") @Column(name = "display_order")
private Integer displayOrder = 0; private Integer displayOrder = 0;
@@ -103,6 +111,22 @@ public class MenuItem extends AbstractAuditingEntity<Long> {
this.displayOrder = displayOrder; this.displayOrder = displayOrder;
} }
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@@ -11,7 +11,7 @@
</delete> </delete>
<insert tableName="sis_page"> <insert tableName="sis_page">
<column name="id" valueSequenceNext="sequenceGenerator"/> <column name="id" valueSequenceNext="sequence_generator"/>
<column name="title" value="Contact Us"/> <column name="title" value="Contact Us"/>
<column name="slug" value="contact-us"/> <column name="slug" value="contact-us"/>
<column name="content" value='{"time": 1721470000000,"blocks":[{"type":"header","data":{"text":"General Information","level":2}},{"type":"paragraph","data":{"text":"For General Campus Inquiries, please call the main switchboard at 413-545-0111. Operators are available from 8:00 a.m. to 5:00 p.m., Monday through Friday."}},{"type":"header","data":{"text":"Directories","level":2}},{"type":"paragraph","data":{"text":"Find contact information for academic departments, campus services, and other resources."}}],"version":"2.29.1"}'/> <column name="content" value='{"time": 1721470000000,"blocks":[{"type":"header","data":{"text":"General Information","level":2}},{"type":"paragraph","data":{"text":"For General Campus Inquiries, please call the main switchboard at 413-545-0111. Operators are available from 8:00 a.m. to 5:00 p.m., Monday through Friday."}},{"type":"header","data":{"text":"Directories","level":2}},{"type":"paragraph","data":{"text":"Find contact information for academic departments, campus services, and other resources."}}],"version":"2.29.1"}'/>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="20260722000000-1" author="antigravity">
<addColumn tableName="sis_menu_item">
<column name="title" type="varchar(255)">
<constraints nullable="true"/>
</column>
<column name="image_url" type="varchar(500)">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -42,4 +42,5 @@
<include file="config/liquibase/changelog/20260718100000_add_doctor_and_specialty.xml" relativeToChangelogFile="false"/> <include file="config/liquibase/changelog/20260718100000_add_doctor_and_specialty.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260718110000_seed_sample_doctors.xml" relativeToChangelogFile="false"/> <include file="config/liquibase/changelog/20260718110000_seed_sample_doctors.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260720181500_seed_contact_us_page.xml" relativeToChangelogFile="false"/> <include file="config/liquibase/changelog/20260720181500_seed_contact_us_page.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260722000000_add_fields_to_menu_item.xml" relativeToChangelogFile="false"/>
</databaseChangeLog> </databaseChangeLog>
@@ -1,4 +1,4 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{fragments/layout}"> layout:decorate="~{fragments/layout}">
@@ -966,7 +966,7 @@
</div> </div>
</main> </main>
<footer th:replace="~{fragments/footer :: footer}"></footer> <footer th:replace="~{themes/__${activeTheme}__/footer :: footer}"></footer>
@@ -1,5 +1,21 @@
<header id="l--main-header"> <header id="l--main-header">
<h1 class="visually-hidden">The University of Massachusetts Amherst</h1> <style>
body { overflow-x: hidden; }
/* Fix mega menu position and width via pure CSS */
.m--main-menu > li { position: static !important; }
#l--main-header .navigation-container,
#l--main-header #mobile-flyout-container,
#l--main-header .primary-navigation { position: static !important; }
.submenu-wrapper {
position: absolute !important;
left: 0 !important;
right: 0 !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box;
}
</style>
<h1 class="visually-hidden">Bệnh Viên Đa Khoa Quốc Tế S.I.S Cần Thơ</h1>
<div class="region region-header r--region r--header"> <div class="region region-header r--region r--header">
@@ -29,7 +29,7 @@
<div class="dialog-off-canvas-main-canvas" data-off-canvas-main-canvas=""> <div class="dialog-off-canvas-main-canvas" data-off-canvas-main-canvas="">
<!-- Header Fragment --> <!-- Header Fragment -->
<div th:replace="~{themes/umass/header}"></div> <div th:replace="~{themes/__${activeTheme}__/header :: header}"></div>
<!-- Main Content Block --> <!-- Main Content Block -->
<main id="main-content" role="main"> <main id="main-content" role="main">
@@ -39,7 +39,7 @@
</main> </main>
<!-- Footer Fragment --> <!-- Footer Fragment -->
<div th:replace="~{themes/umass/footer :: footer}"></div> <div th:replace="~{themes/__${activeTheme}__/footer :: footer}"></div>
</div> </div>
<!-- Core Scripts --> <!-- Core Scripts -->
@@ -107,7 +107,7 @@
style="display:none;visibility:hidden"></iframe></noscript> style="display:none;visibility:hidden"></iframe></noscript>
<div class="dialog-off-canvas-main-canvas" data-off-canvas-main-canvas=""> <div class="dialog-off-canvas-main-canvas" data-off-canvas-main-canvas="">
<header th:replace="~{themes/umass/header :: header}"></header> <header th:replace="~{themes/__${activeTheme}__/header :: header}"></header>
<main id="main-content"> <main id="main-content">
<div class="l--content"> <div class="l--content">
@@ -2737,7 +2737,7 @@
<footer th:replace="~{fragments/footer :: footer}"></footer> <footer th:replace="~{themes/__${activeTheme}__/footer :: footer}"></footer>
</div> </div>
@@ -61,11 +61,27 @@
<label for="url">URL</label> <label for="url">URL</label>
<input type="text" class="form-control" id="url" name="url" placeholder="e.g. /" required> <input type="text" class="form-control" id="url" name="url" placeholder="e.g. /" required>
</div> </div>
<div class="form-group">
<label for="title">Title (Tùy chọn - Dùng cho Mega Menu)</label>
<input type="text" class="form-control" id="title" name="title" placeholder="VD: BỆNH VIỆN...">
</div>
<div class="form-group">
<label for="imageUrl">Image URL (Tùy chọn - Dùng cho Mega Menu)</label>
<input type="text" class="form-control" id="imageUrl" name="imageUrl" placeholder="VD: /images/banner.jpg">
</div>
<div class="form-group"> <div class="form-group">
<label for="parentId">Parent Item (Optional)</label> <label for="parentId">Parent Item (Optional)</label>
<select class="form-control" id="parentId" name="parentId"> <select class="form-control" id="parentId" name="parentId">
<option value="">-- None (Top Level) --</option> <option value="">-- None (Top Level) --</option>
<option th:each="parentItem : ${menu.items}" th:if="${parentItem.parent == null}" th:value="${parentItem.id}" th:text="${parentItem.label}"></option> <th:block th:each="topOpt : ${menu.items}" th:if="${topOpt.parent == null}">
<option th:value="${topOpt.id}" th:text="${topOpt.label}"></option>
<th:block th:each="childOpt : ${menu.items}" th:if="${childOpt.parent != null and childOpt.parent.id == topOpt.id}">
<option th:value="${childOpt.id}" th:text="'-- ' + ${childOpt.label}"></option>
<th:block th:each="grandChildOpt : ${menu.items}" th:if="${grandChildOpt.parent != null and grandChildOpt.parent.id == childOpt.id}">
<option th:value="${grandChildOpt.id}" th:text="'---- ' + ${grandChildOpt.label}"></option>
</th:block>
</th:block>
</th:block>
</select> </select>
</div> </div>
<button type="submit" class="btn btn-primary btn-block">Add to Menu</button> <button type="submit" class="btn btn-primary btn-block">Add to Menu</button>
@@ -116,6 +132,24 @@
</form> </form>
</div> </div>
</li> </li>
<!-- Grandchildren Items -->
<th:block th:each="grandchild : ${menu.items}" th:if="${grandchild.parent != null and grandchild.parent.id == child.id}">
<li class="list-group-item d-flex justify-content-between align-items-center bg-light" style="margin-left: 4rem; border-left: 4px solid #36b9cc;">
<div>
<strong th:text="${grandchild.label}">Link Text</strong>
<span class="badge badge-info ml-2">Sub-child</span>
<br>
<small class="text-muted" th:text="${grandchild.url}">URL</small>
</div>
<div>
<span class="badge badge-primary badge-pill mr-2" th:text="'Order: ' + ${grandchild.displayOrder}">0</span>
<button class="btn btn-sm btn-info" data-toggle="modal" th:data-target="'#editModal-' + ${grandchild.id}">Edit</button>
<form th:action="@{/manage/menus/{menuId}/items/{itemId}/delete(menuId=${menu.id}, itemId=${grandchild.id})}" method="post" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this menu item?');">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
</div>
</li>
</th:block>
</th:block> </th:block>
</th:block> </th:block>
</ul> </ul>
@@ -148,12 +182,26 @@
<label>URL</label> <label>URL</label>
<input type="text" class="form-control" name="url" th:value="${item.url}" required> <input type="text" class="form-control" name="url" th:value="${item.url}" required>
</div> </div>
<div class="form-group">
<label>Title (Tùy chọn)</label>
<input type="text" class="form-control" name="title" th:value="${item.title}">
</div>
<div class="form-group">
<label>Image URL (Tùy chọn)</label>
<input type="text" class="form-control" name="imageUrl" th:value="${item.imageUrl}">
</div>
<div class="form-group"> <div class="form-group">
<label>Parent Item (Optional)</label> <label>Parent Item (Optional)</label>
<select class="form-control" name="parentId"> <select class="form-control" name="parentId">
<option value="">-- None (Top Level) --</option> <option value="">-- None (Top Level) --</option>
<th:block th:each="parentOpt : ${menu.items}" th:if="${parentOpt.parent == null and parentOpt.id != item.id}"> <th:block th:each="topOpt : ${menu.items}" th:if="${topOpt.parent == null and topOpt.id != item.id}">
<option th:value="${parentOpt.id}" th:text="${parentOpt.label}" th:selected="${item.parent != null and item.parent.id == parentOpt.id}"></option> <option th:value="${topOpt.id}" th:text="${topOpt.label}" th:selected="${item.parent != null and item.parent.id == topOpt.id}"></option>
<th:block th:each="childOpt : ${menu.items}" th:if="${childOpt.parent != null and childOpt.parent.id == topOpt.id and childOpt.id != item.id}">
<option th:value="${childOpt.id}" th:text="'-- ' + ${childOpt.label}" th:selected="${item.parent != null and item.parent.id == childOpt.id}"></option>
<th:block th:each="grandChildOpt : ${menu.items}" th:if="${grandChildOpt.parent != null and grandChildOpt.parent.id == childOpt.id and grandChildOpt.id != item.id}">
<option th:value="${grandChildOpt.id}" th:text="'---- ' + ${grandChildOpt.label}" th:selected="${item.parent != null and item.parent.id == grandChildOpt.id}"></option>
</th:block>
</th:block>
</th:block> </th:block>
</select> </select>
</div> </div>
@@ -72,8 +72,8 @@
const rect = menu.getBoundingClientRect(); const rect = menu.getBoundingClientRect();
// Shift wrapper exactly to the left edge of the window // Shift wrapper exactly to the left edge of the window
wrapper.style.left = '-' + rect.left + 'px'; wrapper.style.left = '-' + rect.left + 'px';
// Set width to exact window width // Set width to exact window width (excluding scrollbar to prevent horizontal scroll)
wrapper.style.width = window.innerWidth + 'px'; wrapper.style.width = document.documentElement.clientWidth + 'px';
} }
}); });
} }
@@ -87,10 +87,45 @@
megamenus.forEach(menu => { megamenus.forEach(menu => {
menu.addEventListener('mouseenter', adjustMegaMenus); menu.addEventListener('mouseenter', adjustMegaMenus);
}); });
// Mega Menu Level 2 Tab Logic
// Initialize first items
document.querySelectorAll('.mc--main-menu .megamenu.menu-item').forEach(menu => {
const firstL2 = menu.querySelector('.l2-hover-target');
if (firstL2) {
firstL2.classList.add('active');
const targetId = firstL2.getAttribute('data-target');
const targetContent = document.getElementById(targetId);
if (targetContent) targetContent.style.display = 'block';
}
});
document.querySelectorAll('.l2-hover-target').forEach(target => {
target.addEventListener('mouseenter', function() {
const wrapper = this.closest('.mega-menu-content');
if (!wrapper) return;
// Remove active from all L2 in this menu
wrapper.querySelectorAll('.l2-hover-target').forEach(el => el.classList.remove('active'));
// Hide all L3 content in this menu
wrapper.querySelectorAll('.l3-content').forEach(el => el.style.display = 'none');
// Activate current
this.classList.add('active');
const contentId = this.getAttribute('data-target');
const content = document.getElementById(contentId);
if (content) {
content.style.display = 'block';
}
});
});
}); });
</script> </script>
<style> <style>
body { overflow-x: hidden; }
@media (min-width: 1024px) { @media (min-width: 1024px) {
/* Arrow Rotation */ /* Arrow Rotation */
@@ -195,6 +230,80 @@
object-fit: cover; object-fit: cover;
} }
/* Middle Column for Tabbed Level 2 */
.mega-col-level2 {
flex: 1.2;
border-left: 1px solid #d1d5db;
padding-left: 30px;
max-height: 245px;
overflow-y: auto;
}
.mega-col-level2::-webkit-scrollbar { width: 6px; }
.mega-col-level2::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 10px; }
.mega-col-level2::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 10px; }
.submenu-l2 {
list-style: none;
padding: 0;
margin: 0;
}
.submenu-l2 .menu-item-l2 {
padding: 0;
margin: 0;
}
.submenu-l2 .menu-item-l2 a {
display: block;
padding: 12px 15px;
color: #111827 !important;
text-decoration: none;
font-weight: 600;
font-size: 14px;
border-radius: 6px;
margin-bottom: 4px;
transition: all 0.2s;
}
.submenu-l2 .menu-item-l2.active a {
background-color: #881c1c !important;
color: #ffffff !important;
}
.submenu-l2 .menu-item-l2:not(.active) a:hover {
background-color: #f3f4f6 !important;
color: #111827 !important;
}
/* Right Column for Level 3 Links */
.mega-col-level3-container {
flex: 1;
border-left: 1px solid #d1d5db;
padding-left: 30px;
max-height: 245px;
overflow-y: auto;
}
.mega-col-level3-container::-webkit-scrollbar { width: 6px; }
.mega-col-level3-container::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 10px; }
.mega-col-level3-container::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 10px; }
.submenu-l3 {
list-style: none;
padding: 0;
margin: 0;
}
.submenu-l3 .menu-item-l3 a {
display: block;
padding: 12px 15px;
color: #111827 !important;
text-decoration: none;
font-weight: 600;
font-size: 14px;
border-radius: 6px;
margin-bottom: 4px;
transition: all 0.2s;
}
.submenu-l3 .menu-item-l3 a:hover {
background-color: var(--color-brand) !important;
color: var(--color-white) !important;
}
/* Hide the mobile back button and title on desktop */ /* Hide the mobile back button and title on desktop */
.mc--main-menu .megamenu.menu-item .submenu-wrapper .button-back, .mc--main-menu .megamenu.menu-item .submenu-wrapper .button-back,
.mc--main-menu .megamenu.menu-item .submenu-wrapper .navigation-title { .mc--main-menu .megamenu.menu-item .submenu-wrapper .navigation-title {
@@ -203,18 +312,19 @@
} }
</style> </style>
<header id="l--main-header"> <header id="l--main-header">
<h1 class="visually-hidden">The University of Massachusetts Amherst</h1> <h1 class="visually-hidden">Bệnh viện Đa Khoa Quốc Tế S.I.S Cần Thơ</h1>
<div class="region region-header r--region r--header"> <div class="region region-header r--region r--header" style="background: linear-gradient(0deg, transparent, #000) !important;">
<section data-component-id="umass_base:site-header" aria-label="Site Header"> <section data-component-id="umass_base:site-header" aria-label="Site Header">
<div data-component-id="umass_base:header-branding"> <div class="header-left" style="display: flex; flex-direction: column; align-items: center; width: max-content;">
<a href="/"> <div data-component-id="umass_base:header-branding">
<img th:src="@{/theme-assets/umass/images/logo_white.png}" alt="Sisvietnamvn" style="height: auto; max-width: 250px;" /> <a href="/">
</a> <img th:src="@{/theme-assets/umass/images/logo_white.png}" alt="Sisvietnamvn" style="height: auto; max-width: 250px;" />
</div> </a>
<div class="header-right" style="display: flex; flex-direction: column; align-items: start; flex-grow: 1;">
<div class="header-hospital-name" style="color: var(--color-brand); font-size: 1.4rem; font-weight: bold; text-transform: uppercase; margin-bottom: 5px; -webkit-text-stroke: 1px white;">
Bệnh viện đa khoa quốc tế S.I.S Cần thơ
</div> </div>
<div class="header-hospital-name" style="color: var(--color-brand);font-size: 1.4rem;font-weight: 900;text-transform: uppercase;margin-bottom: 5px;text-shadow: -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 1px 0 white; white-space: nowrap; text-align: center;">
Bệnh viện đa khoa quốc tế <br>S.I.S Cần thơ
</div>
</div>
<div class="navigation-container" style="width: 100%; display: flex; justify-content: flex-end;"> <div class="navigation-container" style="width: 100%; display: flex; justify-content: flex-end;">
<div id="mobile-flyout-container"> <div id="mobile-flyout-container">
<div class="primary-navigation"> <div class="primary-navigation">
@@ -244,7 +354,7 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 10" width="5" height="10"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 10" width="5" height="10">
<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#881c1c" points="0,5 5,0 5,10 "></polygon> <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#881c1c" points="0,5 5,0 5,10 "></polygon>
</svg> Back </button> </svg> Back </button>
<span class="navigation-title" th:text="${item.label}">Menu Item</span> <span class="navigation-title" th:text="${not #strings.isEmpty(item.title) ? item.title : item.label}">Menu Item</span>
<!-- Mobile Submenu Layout (Simple List) --> <!-- Mobile Submenu Layout (Simple List) -->
<ul class="submenu mobile-only"> <ul class="submenu mobile-only">
@@ -257,25 +367,39 @@
<div class="mega-menu-content desktop-only"> <div class="mega-menu-content desktop-only">
<!-- Column 1: Info (Title and Text) --> <!-- Column 1: Info (Title and Text) -->
<div class="mega-col-info"> <div class="mega-col-info">
<h3 th:text="${item.label}">Về Bệnh viện</h3> <h3 th:text="${not #strings.isEmpty(item.title) ? item.title : item.label}">BỆNH VIỆN ĐA KHOA QUỐC TẾ S.I.S CẦN THƠ</h3>
<p th:if="${item.label == 'Về Bệnh viện' or item.label == 'VỀ BỆNH VIỆN'}" style="text-transform: math-auto;">Bệnh viện Đột quỵ Quốc tế tự hào là đơn vị tiên phong trong việc cung cấp dịch vụ khám chữa bệnh chất lượng cao, mang lại sự tin cậy và an tâm cho bệnh nhân.</p> <p th:if="${item.label == 'Về Bệnh viện' or item.label == 'VỀ BỆNH VIỆN'}" style="text-transform: math-auto;">Bệnh viện Đa Khoa Quốc Tế S.I.S Cần Thơ tự hào là đơn vị tiên phong trong việc cung cấp dịch vụ khám chữa bệnh chất lượng cao, mang lại sự tin cậy và an tâm cho bệnh nhân.</p>
<p th:if="${item.label == 'Chuyên khoa' or item.label == 'CHUYÊN KHOA'}" style="text-transform: math-auto;">Khám phá các chuyên khoa hàng đầu với đội ngũ bác sĩ chuyên môn cao, trang thiết bị y tế hiện đại đạt tiêu chuẩn quốc tế.</p> <p th:if="${item.label == 'Chuyên khoa' or item.label == 'CHUYÊN KHOA'}" style="text-transform: math-auto;">Khám phá các chuyên khoa hàng đầu với đội ngũ bác sĩ chuyên môn cao, trang thiết bị y tế hiện đại đạt tiêu chuẩn quốc tế.</p>
<p th:if="${item.label != 'Về Bệnh viện' and item.label != 'VỀ BỆNH VIỆN' and item.label != 'Chuyên khoa' and item.label != 'CHUYÊN KHOA'}" style="text-transform: math-auto;">Khám phá thêm thông tin chi tiết về các dịch vụ, chuyên gia và hoạt động của chúng tôi để bảo vệ sức khỏe của bạn.</p> <p th:if="${item.label != 'Về Bệnh viện' and item.label != 'VỀ BỆNH VIỆN' and item.label != 'Chuyên khoa' and item.label != 'CHUYÊN KHOA'}" style="text-transform: math-auto;">Khám phá thêm thông tin chi tiết về các dịch vụ, chuyên gia và hoạt động của chúng tôi để bảo vệ sức khỏe của bạn.</p>
</div> </div>
<!-- Column 2: The actual Submenu Links --> <!-- Column 2: Level 2 Links -->
<div class="mega-col-links"> <div class="mega-col-level2" th:if="${not #lists.isEmpty(item.children)}">
<ul class="submenu"> <ul class="submenu-l2">
<li class="menu-item" th:each="child : ${item.children}"> <li class="menu-item-l2 l2-hover-target" th:each="child : ${item.children}" th:attr="data-target='l3-' + ${item.id} + '-' + ${child.id}">
<a th:href="${child.url}" th:text="${child.label}">Child Item</a> <a th:href="${child.url}" th:text="${child.label}">Child Item</a>
</li> </li>
</ul> </ul>
</div> </div>
<!-- Column 3: Featured Image --> <!-- Column 3: Level 3 Links OR Fallback Image -->
<div class="mega-col-image"> <div class="mega-col-level3-container" th:if="${not #lists.isEmpty(item.children)}">
<!-- Try to use a hospital-like image if we can, or fallback to logo --> <!-- Render each Level 3 target content -->
<img th:src="@{/theme-assets/umass/images/logo.png}" alt="Featured Image" style="object-fit: contain; background: white; padding: 20px;"/> <th:block th:each="child : ${item.children}">
<ul th:id="'l3-' + ${item.id} + '-' + ${child.id}" class="submenu-l3 l3-content" style="display: none;">
<th:block th:if="${not #lists.isEmpty(child.children)}">
<li class="menu-item-l3" th:each="grandchild : ${child.children}">
<a th:href="${grandchild.url}" th:text="${grandchild.label}">Grandchild Item</a>
</li>
</th:block>
<th:block th:if="${#lists.isEmpty(child.children)}">
<div class="mega-col-image" style="padding-left: 0; border: none; flex: 1;">
<img th:if="${not #strings.isEmpty(item.imageUrl)}" th:src="${item.imageUrl}" style="object-fit: cover; border-radius: 8px; width: 100%;"/>
<img th:if="${#strings.isEmpty(item.imageUrl)}" th:src="@{/theme-assets/umass/images/logo.png}" style="object-fit: contain; background: white; padding: 20px; width: 100%;"/>
</div>
</th:block>
</ul>
</th:block>
</div> </div>
</div> </div>
<!-- End Mega Menu Layout --> <!-- End Mega Menu Layout -->
@@ -303,7 +427,6 @@
<span class="hamburger-inner"></span> <span class="hamburger-inner"></span>
</span> </span>
</button> </button>
</div>
</section> </section>
<div data-component-id="umass_base:header-search"> <div data-component-id="umass_base:header-search">
<div id="search-flyout-container"> <div id="search-flyout-container">