feat: implement doctor API synchronization and add contact page support with media assets
@@ -2,7 +2,7 @@
|
||||
<div class="c--component c--tabbed-media-content">
|
||||
<div class="tabbed-media-content-text-col">
|
||||
<div data-component-id="umass_base:section-title" class="f--section-title">
|
||||
<h2 style="color: var(--color-brand);">LiveStream</h2>
|
||||
<h2 style="color: var(--color-old-brick);">LiveStream</h2>
|
||||
</div>
|
||||
<div data-component-id="umass_base:tabbed-container" data-once="tabbed-container">
|
||||
<div class="tab-labels-container" role="tablist">
|
||||
|
||||
@@ -1,66 +1,205 @@
|
||||
<div class="submenu-wrapper" style="left: -671.344px;width: 1920px;">
|
||||
<button class="button-back">
|
||||
<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>
|
||||
</svg> Back </button>
|
||||
<span class="navigation-title">VỀ BỆNH VIỆN</span>
|
||||
|
||||
<!-- Mobile Submenu Layout (Simple List) -->
|
||||
<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>
|
||||
|
||||
<!-- Mega Menu Content Layout -->
|
||||
<div class="mega-menu-content desktop-only">
|
||||
<!-- Column 1: Info (Title and Text) -->
|
||||
<div class="mega-col-info">
|
||||
<h3>VỀ BỆNH VIỆN</h3>
|
||||
<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>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Column 2: The actual Submenu Links -->
|
||||
<div class="mega-col-links">
|
||||
<ul class="submenu">
|
||||
<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>
|
||||
</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>
|
||||
<!-- End Mega Menu Layout -->
|
||||
<style>
|
||||
.education-slider-wrapper {
|
||||
position: relative;
|
||||
padding: 0px;
|
||||
/* Space for buttons */
|
||||
}
|
||||
|
||||
</div>
|
||||
.education-slider-container {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.education-slider-wrapper .news-grid,
|
||||
.education-slider-wrapper .row,
|
||||
.education-slider-track {
|
||||
display: flex !important;
|
||||
flex-wrap: nowrap !important;
|
||||
gap: 20px;
|
||||
margin: 0 !important;
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
|
||||
/* Force children to be fixed width cards */
|
||||
.education-slider-wrapper .news-grid>*,
|
||||
.education-slider-wrapper .row>*,
|
||||
.education-slider-track>* {
|
||||
flex: 0 0 calc(33.333% - 14px) !important;
|
||||
/* show 3 cards at a time */
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
|
||||
.education-slider-wrapper .news-grid>*,
|
||||
.education-slider-wrapper .row>*,
|
||||
.education-slider-track>* {
|
||||
flex: 0 0 calc(50% - 10px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
|
||||
.education-slider-wrapper .news-grid>*,
|
||||
.education-slider-wrapper .row>*,
|
||||
.education-slider-track>* {
|
||||
flex: 0 0 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.edu-slider-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(200, 200, 200, 0.5);
|
||||
/* mờ / transparent */
|
||||
color: #333;
|
||||
border: none;
|
||||
font-size: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.edu-slider-btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.edu-slider-btn.prev-btn {
|
||||
left: -10px;
|
||||
}
|
||||
|
||||
.edu-slider-btn.next-btn {
|
||||
right: -10px;
|
||||
}
|
||||
|
||||
.edu-slider-btn:disabled {
|
||||
opacity: 0.2;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* --- Hiệu ứng crop (zoom) ảnh khi hover --- */
|
||||
.education-slider-wrapper .featured-grid__item .news-card__image {
|
||||
overflow: hidden !important;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.education-slider-wrapper .featured-grid__item .news-card__image img {
|
||||
transition: transform 0.4s ease-in-out !important;
|
||||
}
|
||||
|
||||
.education-slider-wrapper .featured-grid__item:hover .news-card__image img {
|
||||
transform: scale(1.1) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<div class="cc--component-container cc--news-events">
|
||||
<div class="c--component c--news-events">
|
||||
<!-- Header -->
|
||||
<div class="news-events-row">
|
||||
<div class="education-column news-column" style="width: 100%;">
|
||||
<div
|
||||
style="display: flex; justify-content: space-between; align-items: baseline; border-bottom: 2px solid var(--color-brand); margin-bottom: 16px;">
|
||||
<!-- Thêm flex-grow: 1 vào style của h2 -->
|
||||
<h2 class="section-title--news"
|
||||
style="font-size: 2rem; margin: 0; padding-bottom: 4px; border-bottom: unset;">ĐÀO TẠO</h2>
|
||||
<a href="/news/education" class="news-events-section__cta news-events-section__cta--desktop"
|
||||
style="color: var(--color-brand); font-weight: 800; font-size: 16px; text-transform: uppercase; text-decoration: none; border-bottom: unset;">XEM
|
||||
THÊM</a>
|
||||
</div>
|
||||
<div class="education-slider-wrapper" style="padding-right: 0;">
|
||||
<button class="edu-slider-btn prev-btn" aria-label="Previous">‹</button>
|
||||
<div class="education-slider-container">
|
||||
<div class="education-slider-track">
|
||||
[component:news-grid data-source="posts:tag=education,limit=5,collection=news_posts"]
|
||||
</div>
|
||||
</div>
|
||||
<button class="edu-slider-btn next-btn" aria-label="Next">›</button>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- Mobile Footer CTA -->
|
||||
<div class="news-events-section__footer"><a href="/news/education"
|
||||
class="news-events-section__cta news-events-section__cta--mobile">Xem Thêm</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const wrappers = document.querySelectorAll('.education-slider-wrapper');
|
||||
wrappers.forEach(wrapper => {
|
||||
const prevBtn = wrapper.querySelector('.prev-btn');
|
||||
const nextBtn = wrapper.querySelector('.next-btn');
|
||||
|
||||
let track = wrapper.querySelector('.education-slider-track');
|
||||
if (track && track.children.length === 1 && (track.children[0].classList.contains('news-grid') || track.children[0].classList.contains('row'))) {
|
||||
track = track.children[0];
|
||||
track.classList.add('education-slider-track');
|
||||
}
|
||||
|
||||
if (!track) return;
|
||||
|
||||
let currentIndex = 0;
|
||||
|
||||
function updateSlider() {
|
||||
const items = track.children;
|
||||
if(items.length === 0) return;
|
||||
const itemWidth = items[0].getBoundingClientRect().width;
|
||||
|
||||
const gap = parseFloat(window.getComputedStyle(track).gap) || 20;
|
||||
track.style.transform = `translateX(-${currentIndex * (itemWidth + gap)}px)`;
|
||||
|
||||
const containerWidth = wrapper.querySelector('.education-slider-container').getBoundingClientRect().width;
|
||||
const visibleCount = Math.max(1, Math.floor((containerWidth + gap) / (itemWidth + gap)));
|
||||
|
||||
prevBtn.disabled = currentIndex === 0;
|
||||
nextBtn.disabled = currentIndex >= items.length - visibleCount;
|
||||
}
|
||||
|
||||
prevBtn.addEventListener('click', () => {
|
||||
if (currentIndex > 0) {
|
||||
currentIndex--;
|
||||
updateSlider();
|
||||
}
|
||||
});
|
||||
|
||||
nextBtn.addEventListener('click', () => {
|
||||
const items = track.children;
|
||||
const itemWidth = items[0].getBoundingClientRect().width;
|
||||
const gap = parseFloat(window.getComputedStyle(track).gap) || 20;
|
||||
const containerWidth = wrapper.querySelector('.education-slider-container').getBoundingClientRect().width;
|
||||
const visibleCount = Math.max(1, Math.floor((containerWidth + gap) / (itemWidth + gap)));
|
||||
|
||||
if (currentIndex < items.length - visibleCount) {
|
||||
currentIndex++;
|
||||
updateSlider();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
const items = track.children;
|
||||
if(items.length === 0) return;
|
||||
const itemWidth = items[0].getBoundingClientRect().width;
|
||||
const gap = parseFloat(window.getComputedStyle(track).gap) || 20;
|
||||
const containerWidth = wrapper.querySelector('.education-slider-container').getBoundingClientRect().width;
|
||||
const visibleCount = Math.max(1, Math.floor((containerWidth + gap) / (itemWidth + gap)));
|
||||
|
||||
if (currentIndex > items.length - visibleCount) {
|
||||
currentIndex = Math.max(0, items.length - visibleCount);
|
||||
}
|
||||
updateSlider();
|
||||
});
|
||||
|
||||
setTimeout(updateSlider, 200);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -19,8 +19,11 @@ import org.springframework.core.env.Environment;
|
||||
import tech.jhipster.config.DefaultProfileUtil;
|
||||
import tech.jhipster.config.JHipsterConstants;
|
||||
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableConfigurationProperties({ LiquibaseProperties.class, ApplicationProperties.class })
|
||||
@EnableScheduling
|
||||
public class SisvietnamvnApp {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SisvietnamvnApp.class);
|
||||
|
||||
@@ -36,15 +36,18 @@ public class PageController {
|
||||
private final com.sisvietnamvn.web.service.HtmlSnippetService snippetService;
|
||||
private final com.sisvietnamvn.web.service.DoctorService doctorService;
|
||||
private final com.sisvietnamvn.web.service.SpecialtyService specialtyService;
|
||||
private final com.sisvietnamvn.web.service.DoctorApiSyncService doctorApiSyncService;
|
||||
|
||||
public PageController(PageService pageService, ObjectMapper objectMapper, HookManager hookManager, com.sisvietnamvn.web.service.HtmlSnippetService snippetService,
|
||||
com.sisvietnamvn.web.service.DoctorService doctorService, com.sisvietnamvn.web.service.SpecialtyService specialtyService) {
|
||||
com.sisvietnamvn.web.service.DoctorService doctorService, com.sisvietnamvn.web.service.SpecialtyService specialtyService,
|
||||
com.sisvietnamvn.web.service.DoctorApiSyncService doctorApiSyncService) {
|
||||
this.pageService = pageService;
|
||||
this.objectMapper = objectMapper;
|
||||
this.hookManager = hookManager;
|
||||
this.snippetService = snippetService;
|
||||
this.doctorService = doctorService;
|
||||
this.specialtyService = specialtyService;
|
||||
this.doctorApiSyncService = doctorApiSyncService;
|
||||
}
|
||||
|
||||
@GetMapping("/page/{slug}")
|
||||
@@ -74,7 +77,12 @@ public class PageController {
|
||||
|
||||
@GetMapping("/doctor")
|
||||
public String getDoctor(Model model) {
|
||||
model.addAttribute("doctors", doctorService.findAll());
|
||||
try {
|
||||
model.addAttribute("doctors", doctorApiSyncService.getCachedDoctors());
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to load doctor API data", e);
|
||||
model.addAttribute("doctors", doctorService.findAll());
|
||||
}
|
||||
model.addAttribute("specialties", specialtyService.findAll());
|
||||
return "doctor";
|
||||
}
|
||||
|
||||
@@ -46,8 +46,10 @@ public class ManageMediaController {
|
||||
@GetMapping
|
||||
public String listMedia(@RequestParam(value = "type", required = false) String typeStr,
|
||||
@RequestParam(value = "keyword", required = false) String keyword,
|
||||
@RequestParam(value = "page", defaultValue = "0") int page,
|
||||
@RequestParam(value = "size", defaultValue = "20") int size,
|
||||
Model model) {
|
||||
LOG.debug("Request to list all media (type={}, keyword={})", typeStr, keyword);
|
||||
LOG.debug("Request to list all media (type={}, keyword={}, page={}, size={})", typeStr, keyword, page, size);
|
||||
|
||||
MediaType type = null;
|
||||
if (typeStr != null && !typeStr.isBlank()) {
|
||||
@@ -58,10 +60,15 @@ public class ManageMediaController {
|
||||
}
|
||||
}
|
||||
|
||||
model.addAttribute("mediaList", mediaService.findFiltered(type, keyword));
|
||||
org.springframework.data.domain.Pageable pageable = org.springframework.data.domain.PageRequest.of(page, size);
|
||||
org.springframework.data.domain.Page<Media> mediaPage = mediaService.findFiltered(type, keyword, pageable);
|
||||
|
||||
model.addAttribute("mediaPage", mediaPage);
|
||||
model.addAttribute("mediaList", mediaPage.getContent());
|
||||
model.addAttribute("mediaTypes", MediaType.values());
|
||||
model.addAttribute("selectedType", typeStr);
|
||||
model.addAttribute("keyword", keyword);
|
||||
model.addAttribute("pageSize", size);
|
||||
return "manage/media/list";
|
||||
}
|
||||
|
||||
@@ -160,4 +167,23 @@ public class ManageMediaController {
|
||||
redirectAttributes.addFlashAttribute("successMessage", "Media deleted successfully!");
|
||||
return "redirect:/manage/media";
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /manage/media/bulk-delete — Delete multiple media items.
|
||||
*/
|
||||
@PostMapping("/bulk-delete")
|
||||
public String bulkDeleteMedia(@RequestParam(value = "mediaIds", required = false) java.util.List<Long> mediaIds, RedirectAttributes redirectAttributes) {
|
||||
LOG.debug("Request to bulk delete media: {}", mediaIds);
|
||||
if (mediaIds != null && !mediaIds.isEmpty()) {
|
||||
int count = 0;
|
||||
for (Long id : mediaIds) {
|
||||
mediaService.delete(id);
|
||||
count++;
|
||||
}
|
||||
redirectAttributes.addFlashAttribute("successMessage", count + " media item(s) deleted successfully!");
|
||||
} else {
|
||||
redirectAttributes.addFlashAttribute("errorMessage", "No media items selected for deletion.");
|
||||
}
|
||||
return "redirect:/manage/media";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.sisvietnamvn.web.repository;
|
||||
import com.sisvietnamvn.web.domain.Media;
|
||||
import com.sisvietnamvn.web.domain.MediaType;
|
||||
import java.util.List;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@@ -12,11 +14,14 @@ import org.springframework.stereotype.Repository;
|
||||
@Repository
|
||||
public interface MediaRepository extends JpaRepository<Media, Long> {
|
||||
|
||||
Page<Media> findAllByOrderByCreatedDateDesc(Pageable pageable);
|
||||
|
||||
Page<Media> findByMediaTypeOrderByCreatedDateDesc(MediaType mediaType, Pageable pageable);
|
||||
|
||||
Page<Media> findByOriginalFilenameContainingIgnoreCaseOrderByCreatedDateDesc(String keyword, Pageable pageable);
|
||||
|
||||
Page<Media> findByMediaTypeAndOriginalFilenameContainingIgnoreCaseOrderByCreatedDateDesc(MediaType mediaType, String keyword, Pageable pageable);
|
||||
|
||||
// Also keep the List version for findAll() if needed
|
||||
List<Media> findAllByOrderByCreatedDateDesc();
|
||||
|
||||
List<Media> findByMediaTypeOrderByCreatedDateDesc(MediaType mediaType);
|
||||
|
||||
List<Media> findByOriginalFilenameContainingIgnoreCaseOrderByCreatedDateDesc(String keyword);
|
||||
|
||||
List<Media> findByMediaTypeAndOriginalFilenameContainingIgnoreCaseOrderByCreatedDateDesc(MediaType mediaType, String keyword);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.sisvietnamvn.web.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import com.sisvietnamvn.web.repository.MediaRepository;
|
||||
|
||||
@Service
|
||||
public class DoctorApiSyncService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DoctorApiSyncService.class);
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private final ObjectMapper objectMapper;
|
||||
private final MediaRepository mediaRepository;
|
||||
|
||||
// In-memory cache
|
||||
private List<Map<String, Object>> cachedDoctors = new ArrayList<>();
|
||||
|
||||
public DoctorApiSyncService(ObjectMapper objectMapper, MediaRepository mediaRepository) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.mediaRepository = mediaRepository;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getCachedDoctors() {
|
||||
return cachedDoctors;
|
||||
}
|
||||
|
||||
// Run immediately after startup and every 30 seconds
|
||||
@PostConstruct
|
||||
@Scheduled(fixedRate = 30000)
|
||||
public void syncDoctorsFromApi() {
|
||||
try {
|
||||
LOG.info("Starting API sync for doctors...");
|
||||
|
||||
// 1. Get Token
|
||||
String tokenUrl = "https://dhs.sisvietnam.vn/connect/token";
|
||||
HttpHeaders tokenHeaders = new HttpHeaders();
|
||||
tokenHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
tokenHeaders.add("Cookie", ".AspNetCore.Culture=c%3Den%7Cuic%3Den; __tenant=3a0f747c-57b6-753b-ba21-3987e8e93b9a");
|
||||
|
||||
MultiValueMap<String, String> tokenBody = new LinkedMultiValueMap<>();
|
||||
tokenBody.add("grant_type", "client_credentials");
|
||||
tokenBody.add("client_id", "DigitalHealthSolutions_LabConn");
|
||||
tokenBody.add("client_secret", "AcEnBCaphERGOGYMaStATITEnaPETYpR");
|
||||
tokenBody.add("scope", "LabConn");
|
||||
|
||||
HttpEntity<MultiValueMap<String, String>> tokenRequest = new HttpEntity<>(tokenBody, tokenHeaders);
|
||||
ResponseEntity<String> tokenResponse = restTemplate.postForEntity(tokenUrl, tokenRequest, String.class);
|
||||
|
||||
JsonNode tokenNode = objectMapper.readTree(tokenResponse.getBody());
|
||||
String accessToken = tokenNode.get("access_token").asText();
|
||||
|
||||
// 2. Fetch doctors
|
||||
String apiUrl = "https://dhs.sisvietnam.vn/api/app/his/doctors/work-schedules";
|
||||
HttpHeaders apiHeaders = new HttpHeaders();
|
||||
apiHeaders.setBearerAuth(accessToken);
|
||||
apiHeaders.add("__tenant", "3a0f747c-57b6-753b-ba21-3987e8e93b9a");
|
||||
apiHeaders.add("Cookie", ".AspNetCore.Culture=c%3Den%7Cuic%3Den; __tenant=3a0f747c-57b6-753b-ba21-3987e8e93b9a");
|
||||
|
||||
HttpEntity<Void> apiRequest = new HttpEntity<>(apiHeaders);
|
||||
ResponseEntity<String> apiResponse = restTemplate.exchange(apiUrl, HttpMethod.GET, apiRequest, String.class);
|
||||
|
||||
JsonNode root = objectMapper.readTree(apiResponse.getBody());
|
||||
JsonNode doctorsNode = root.at("/data/doctors");
|
||||
|
||||
List<Map<String, Object>> newDoctorsList = new ArrayList<>();
|
||||
if (doctorsNode.isArray()) {
|
||||
for (JsonNode node : doctorsNode) {
|
||||
Map<String, Object> doc = new HashMap<>();
|
||||
doc.put("title", node.get("title") != null && !node.get("title").isNull() ? node.get("title").asText("") : "BS");
|
||||
String fullName = node.get("fullName") != null && !node.get("fullName").isNull() ? node.get("fullName").asText("") : "";
|
||||
String title = doc.get("title").toString();
|
||||
if (fullName.startsWith(title)) {
|
||||
fullName = fullName.substring(title.length()).trim();
|
||||
if (fullName.startsWith(".")) fullName = fullName.substring(1).trim();
|
||||
}
|
||||
doc.put("name", fullName);
|
||||
String doctorCode = node.get("doctorCode") != null && !node.get("doctorCode").isNull() ? node.get("doctorCode").asText() : "";
|
||||
doc.put("doctorCode", doctorCode);
|
||||
doc.put("bookingUrl", "#");
|
||||
|
||||
String avatarUrl = "/images/default-avatar.png";
|
||||
if (!doctorCode.isEmpty()) {
|
||||
org.springframework.data.domain.Page<com.sisvietnamvn.web.domain.Media> mediaPage = mediaRepository.findByOriginalFilenameContainingIgnoreCaseOrderByCreatedDateDesc(doctorCode, org.springframework.data.domain.PageRequest.of(0, 1));
|
||||
if (mediaPage.hasContent()) {
|
||||
avatarUrl = mediaPage.getContent().get(0).getFileUrl();
|
||||
LOG.info("Matched avatar for doctor {}: {}", doctorCode, avatarUrl);
|
||||
} else {
|
||||
LOG.info("No avatar found for doctor {}", doctorCode);
|
||||
}
|
||||
}
|
||||
doc.put("avatarUrl", avatarUrl);
|
||||
|
||||
Map<String, String> spec = new HashMap<>();
|
||||
spec.put("name", "Đa khoa");
|
||||
spec.put("iconUrl", "");
|
||||
doc.put("specialty", spec);
|
||||
newDoctorsList.add(doc);
|
||||
}
|
||||
}
|
||||
|
||||
// Atomically swap the reference
|
||||
if (!newDoctorsList.isEmpty()) {
|
||||
newDoctorsList.sort((d1, d2) -> {
|
||||
String c1 = (String) d1.get("doctorCode");
|
||||
String c2 = (String) d2.get("doctorCode");
|
||||
int r1 = "00001".equals(c1) ? 0 : ("00002".equals(c1) ? 1 : 2);
|
||||
int r2 = "00001".equals(c2) ? 0 : ("00002".equals(c2) ? 1 : 2);
|
||||
return Integer.compare(r1, r2);
|
||||
});
|
||||
this.cachedDoctors = newDoctorsList;
|
||||
LOG.info("Successfully synced {} doctors to cache.", newDoctorsList.size());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to sync doctors from API", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import java.nio.file.StandardCopyOption;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -37,6 +36,43 @@ public class MediaService {
|
||||
this.mediaRepository = mediaRepository;
|
||||
}
|
||||
|
||||
@jakarta.annotation.PostConstruct
|
||||
public void syncDiskToDb() {
|
||||
LOG.info("Starting synchronization of disk files to Media database...");
|
||||
try {
|
||||
Path baseDir = Paths.get(BASE_UPLOAD_DIR);
|
||||
if (!Files.exists(baseDir)) return;
|
||||
|
||||
java.nio.file.Files.walk(baseDir)
|
||||
.filter(java.nio.file.Files::isRegularFile)
|
||||
.forEach(path -> {
|
||||
try {
|
||||
String relativePath = "/" + baseDir.relativize(path).toString().replace("\\", "/");
|
||||
String fileUrl = "/" + BASE_UPLOAD_DIR + relativePath;
|
||||
|
||||
// Check if it already exists in DB
|
||||
boolean exists = mediaRepository.findAll().stream()
|
||||
.anyMatch(m -> fileUrl.equals(m.getFileUrl()));
|
||||
|
||||
if (!exists) {
|
||||
String filename = path.getFileName().toString();
|
||||
Media media = new Media();
|
||||
media.setFileUrl(fileUrl);
|
||||
media.setOriginalFilename(filename);
|
||||
media.setMediaType(MediaType.IMAGE); // Assume image for now
|
||||
|
||||
mediaRepository.save(media);
|
||||
LOG.info("Synced missing file to database: {}", fileUrl);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error syncing file: " + path, e);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error walking uploads directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to disk and persist a Media entity.
|
||||
* Files are stored in uploads/YYYY/MM/ based on the current date.
|
||||
@@ -51,17 +87,34 @@ public class MediaService {
|
||||
Files.createDirectories(uploadDir);
|
||||
}
|
||||
|
||||
// Generate unique stored filename
|
||||
String originalFilename = StringUtils.cleanPath(file.getOriginalFilename());
|
||||
// Generate stored filename using original filename
|
||||
String originalFilename = StringUtils.cleanPath(file.getOriginalFilename() != null ? file.getOriginalFilename() : "file");
|
||||
String baseName = originalFilename;
|
||||
String extension = "";
|
||||
int dotIndex = originalFilename.lastIndexOf('.');
|
||||
if (dotIndex > 0) {
|
||||
baseName = originalFilename.substring(0, dotIndex);
|
||||
extension = originalFilename.substring(dotIndex);
|
||||
}
|
||||
String storedFilename = UUID.randomUUID().toString() + extension;
|
||||
|
||||
// Sanitize baseName (remove invalid filesystem characters)
|
||||
baseName = baseName.replaceAll("[\\\\/:*?\"<>|]", "_");
|
||||
if (baseName.isBlank()) {
|
||||
baseName = "file";
|
||||
}
|
||||
|
||||
String storedFilename = baseName + extension;
|
||||
Path filePath = uploadDir.resolve(storedFilename);
|
||||
|
||||
// Handle collision: if file with same name exists, append -1, -2, etc.
|
||||
int count = 1;
|
||||
while (Files.exists(filePath)) {
|
||||
storedFilename = baseName + "-" + count + extension;
|
||||
filePath = uploadDir.resolve(storedFilename);
|
||||
count++;
|
||||
}
|
||||
|
||||
// Save file to disk
|
||||
Path filePath = uploadDir.resolve(storedFilename);
|
||||
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
// Build the public URL
|
||||
@@ -93,22 +146,22 @@ public class MediaService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Find media filtered by type and/or keyword.
|
||||
* Find media filtered by type and/or keyword, with pagination.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Media> findFiltered(MediaType type, String keyword) {
|
||||
LOG.debug("Request to get filtered Media (type={}, keyword={})", type, keyword);
|
||||
public org.springframework.data.domain.Page<Media> findFiltered(MediaType type, String keyword, org.springframework.data.domain.Pageable pageable) {
|
||||
LOG.debug("Request to get filtered Media (type={}, keyword={}, pageable={})", type, keyword, pageable);
|
||||
boolean hasType = type != null;
|
||||
boolean hasKeyword = keyword != null && !keyword.isBlank();
|
||||
|
||||
if (hasType && hasKeyword) {
|
||||
return mediaRepository.findByMediaTypeAndOriginalFilenameContainingIgnoreCaseOrderByCreatedDateDesc(type, keyword);
|
||||
return mediaRepository.findByMediaTypeAndOriginalFilenameContainingIgnoreCaseOrderByCreatedDateDesc(type, keyword, pageable);
|
||||
} else if (hasType) {
|
||||
return mediaRepository.findByMediaTypeOrderByCreatedDateDesc(type);
|
||||
return mediaRepository.findByMediaTypeOrderByCreatedDateDesc(type, pageable);
|
||||
} else if (hasKeyword) {
|
||||
return mediaRepository.findByOriginalFilenameContainingIgnoreCaseOrderByCreatedDateDesc(keyword);
|
||||
return mediaRepository.findByOriginalFilenameContainingIgnoreCaseOrderByCreatedDateDesc(keyword, pageable);
|
||||
} else {
|
||||
return mediaRepository.findAllByOrderByCreatedDateDesc();
|
||||
return mediaRepository.findAllByOrderByCreatedDateDesc(pageable);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</delete>
|
||||
|
||||
<insert tableName="sis_page">
|
||||
<column name="id" valueSequenceNext="sequence_generator"/>
|
||||
<column name="id" valueComputed="sequence_generator.nextval"/>
|
||||
<column name="title" 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"}'/>
|
||||
|
||||
@@ -86,7 +86,7 @@ body, h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
|
||||
color: #000000 !important;
|
||||
}
|
||||
.cc--tabbed-media-content .f--description a {
|
||||
color: var(--color-brand) !important;
|
||||
color: var(--color-old-brick) !important;
|
||||
}
|
||||
|
||||
.cc--tabbed-media-content .tab-labels-inner {
|
||||
@@ -587,3 +587,27 @@ body, h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
|
||||
.bg-linear-hero {
|
||||
background: linear-gradient(180deg, transparent, #000) !important;
|
||||
}
|
||||
|
||||
.doctor-page #l--main-header {
|
||||
background-color: var(--color-old-brick);
|
||||
}
|
||||
|
||||
.doctor-page #l--main-header a{
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.transparent-header .region-header {
|
||||
background: linear-gradient(0deg, transparent, #000);
|
||||
}
|
||||
|
||||
.doctor-page #pagination-controls button.is-active {
|
||||
background-color: var(--color-old-brick);
|
||||
}
|
||||
|
||||
.header-hospital-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.umass-platform-homepage .header-hospital-name {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,966 @@
|
||||
{
|
||||
"doctors_work_schedules": {
|
||||
"data": {
|
||||
"fromDate": "2026-07-22",
|
||||
"toDate": "2026-08-05",
|
||||
"doctors": [
|
||||
{
|
||||
"doctorId": "1149ab21-be72-4977-ba84-34216ae0ba3d",
|
||||
"doctorCode": "00304",
|
||||
"fullName": "BS CKI. LÂM HỮU NGHĨA",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "007004/CT-CCHN",
|
||||
"title": "BS CKI."
|
||||
},
|
||||
{
|
||||
"doctorId": "b3909369-db82-43f1-8da7-ad643133a51e",
|
||||
"doctorCode": "00275",
|
||||
"fullName": "BS CKI. NGUYỄN QUANG HƯNG",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "0026102/BYT-CCHN",
|
||||
"title": "BS CKI."
|
||||
},
|
||||
{
|
||||
"doctorId": "feaf54ac-e8c2-42a5-bb3b-65e9ba7fc2a7",
|
||||
"doctorCode": "00001",
|
||||
"fullName": "TS. BS. TRẦN CHÍ CƯỜNG",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "001257/BYT-CCHN",
|
||||
"title": "TS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "198ee2b2-67a9-4d72-91f5-0ff61fdf514d",
|
||||
"doctorCode": "00528",
|
||||
"fullName": "BS. NGUYỄN ĐỨC CẢNH",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "007403/CT-CCHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "0ae59c64-3950-4560-89ae-b15ed38cb0cf",
|
||||
"doctorCode": "00527",
|
||||
"fullName": "BS. TÔN NỮ THỊ ĐIỂM",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "007555/CT-CCHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "e1c59689-1082-4f0d-bc12-ea97453e83b7",
|
||||
"doctorCode": "00526",
|
||||
"fullName": "BS CKI. TRẦN THỊ THANH THÀ",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "007394/CT-CCHN",
|
||||
"title": "BS CKI."
|
||||
},
|
||||
{
|
||||
"doctorId": "59b84807-35ea-4b76-86e6-aaba0d784aec",
|
||||
"doctorCode": "00233",
|
||||
"fullName": "THS. BS. TRẦN MINH LUẬN",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "0026985/BYT-CCHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "65ed56a0-b749-4de1-a96c-58988613dcbe",
|
||||
"doctorCode": "00055",
|
||||
"fullName": "BS CKI. TRẦN ÁI THANH",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "003618/BYT-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "21fbd4a5-7d36-4288-98ed-d59c6241607e",
|
||||
"doctorCode": "00237",
|
||||
"fullName": "THS. BS. NGUYỄN ĐÀO NHẬT HUY",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "0026660/BYT-CCHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "3ed44686-0d97-4275-bc7b-8023cd40e5bf",
|
||||
"doctorCode": "00313",
|
||||
"fullName": "THS. BS. TRƯƠNG PHẠM VĨNH LỄ",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000377/HAUG-CCHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "92b545fc-39b4-4e6a-b7ac-d2c2a022f490",
|
||||
"doctorCode": "00114",
|
||||
"fullName": "THS. BS. TRẦM THỊ KIM SA",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "001370/CT-CCHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "824cd857-1911-4542-b8d5-1e1644f4c663",
|
||||
"doctorCode": "00302",
|
||||
"fullName": "BS. NGUYỄN HỮU THƠ",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "0003359/VL-CCHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "353258d5-9c7f-4180-8abc-220d8cf57d84",
|
||||
"doctorCode": "00427",
|
||||
"fullName": "BS CKI. DANH THỊ THOA",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "0032307/HCM-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "4e7bde5d-df6a-4bb5-aed8-11b7dcdd4a32",
|
||||
"doctorCode": "HT00527",
|
||||
"fullName": "BS. NGUYỄN TRÂN TRÂN",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "3132d8d4-2c62-4352-a282-70b1bb6a006a",
|
||||
"doctorCode": "00306",
|
||||
"fullName": "THS. BS. NGUYỄN HẢI NGUYÊN",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "028894/BYT-CCHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "e5786fb1-b8ee-43c0-8195-7b8ecb50206b",
|
||||
"doctorCode": "HT0385",
|
||||
"fullName": "BS CKII. VƯƠNG THỊ NGUYÊN CHI",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "003605/BYT-CCHN",
|
||||
"title": "BS CKII"
|
||||
},
|
||||
{
|
||||
"doctorId": "baecc838-f2bb-4673-9ae0-2eba81b1291a",
|
||||
"doctorCode": "00458",
|
||||
"fullName": "BS CKI. PHAN THỊ THU",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000379/LA-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "f4c48f6e-a517-4cbf-a8af-6539037d9955",
|
||||
"doctorCode": "00358",
|
||||
"fullName": "THS. BS. LÊ MINH THẮNG",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "007148/CT-CCHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "6615add7-962d-4d28-b121-5b3ab725dc01",
|
||||
"doctorCode": "00470",
|
||||
"fullName": "BS CKI. NGUYỄN LÂM GIANG",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "0004328/VL-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "1c0825ce-e9ee-44b8-9865-b7a6bc62c0ea",
|
||||
"doctorCode": "00194",
|
||||
"fullName": "BS CKII. NGUYỄN MẠNH CƯỜNG",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "003192/BD-CCHN",
|
||||
"title": "BS CKII"
|
||||
},
|
||||
{
|
||||
"doctorId": "01164889-8323-4036-8a31-486a12b74297",
|
||||
"doctorCode": "00492",
|
||||
"fullName": "BS CKI. MAI VĂN MUỐNG",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "007269/AG-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "ee098817-ef39-42a4-a4be-2f7ecafbffca",
|
||||
"doctorCode": "00357",
|
||||
"fullName": "THS. BS. NGUYỄN KIM PHỤNG",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "005665/CT-CCHN",
|
||||
"title": "THS. BS."
|
||||
},
|
||||
{
|
||||
"doctorId": "c1ff2126-e26f-415b-a82d-84602c84ae28",
|
||||
"doctorCode": "00070",
|
||||
"fullName": "BS CKII. HUỲNH QUỐC SĨ",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "002968/HAUG-CCHN",
|
||||
"title": "BS CKII"
|
||||
},
|
||||
{
|
||||
"doctorId": "b3b8e139-d6c7-4e94-b561-a97bf86b750c",
|
||||
"doctorCode": "00420",
|
||||
"fullName": "BS CKI. THÁI THỊ XUÂN PHƯƠNG",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "002686/CT-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "c9446f3d-54d4-4d6e-84a9-2a5a2f7a5c68",
|
||||
"doctorCode": "01079",
|
||||
"fullName": "BS CKI. VÕ VĂN NĂM",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000139/CT-GPHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "1f173657-3e51-4ed1-8e17-effd74edf4a5",
|
||||
"doctorCode": "00002",
|
||||
"fullName": "BS CKII. PHAN TRỊNH MINH HIẾU",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "001519/BYT-CCHN",
|
||||
"title": "BS CKII"
|
||||
},
|
||||
{
|
||||
"doctorId": "33e6ab26-31cb-4d5e-a235-638bd1326081",
|
||||
"doctorCode": "00081",
|
||||
"fullName": "BS CKI. LÂM THÀNH LUÂN",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "006262/CT-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "65eca9e4-fb28-4fbe-a3af-b1ae649b51ad",
|
||||
"doctorCode": "00372",
|
||||
"fullName": "BS CKI. TRẦN TIẾN THÀNH",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "005270/CT-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "e9f3be09-f287-4656-aef5-a721ecc3d8c4",
|
||||
"doctorCode": "00808",
|
||||
"fullName": "THS. BS. NGUYỄN TRẦN DUY",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "004745/CM-CCHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "b14220b3-dbba-4a1e-8ae0-8dff1c770de4",
|
||||
"doctorCode": "00256",
|
||||
"fullName": "BS CKII. CHÂU THỊ THÚY LIỄU",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000963/TV-CCHN",
|
||||
"title": "BS CKII"
|
||||
},
|
||||
{
|
||||
"doctorId": "4d088627-be04-4a1d-8c91-4fb9ba8657de",
|
||||
"doctorCode": "00949",
|
||||
"fullName": "BS CKII. NGÔ MIÊN TƯỜNG",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000837/CT-CCHN",
|
||||
"title": "BS CKII"
|
||||
},
|
||||
{
|
||||
"doctorId": "93bdb78a-0b67-4205-bfa7-b31c33b1b527",
|
||||
"doctorCode": "00951",
|
||||
"fullName": "BS CKI. VŨ YẾN NHI",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "0004985/VL-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "101fd692-e9e3-44c2-b8e1-f18f6f1c3572",
|
||||
"doctorCode": "00958",
|
||||
"fullName": "BS CKI. TRƯƠNG LÊ ANH KIỆT",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "007730/CT-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "14df442c-51dc-4c84-8c96-fb22712c581a",
|
||||
"doctorCode": "00961",
|
||||
"fullName": "BS CKI. PHẠM THỊ THU THẢO",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "0005762/BTR-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "f8f4fefc-c0af-4366-a36c-393f7b8974c5",
|
||||
"doctorCode": "00825",
|
||||
"fullName": "BS CKI. HUỲNH ANH",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "003099/HAUG-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "dd065952-2ddc-4d02-8f44-6dfee21dc789",
|
||||
"doctorCode": "00123",
|
||||
"fullName": "BS CKII. NGUYỄN ANH TRUNG",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "001590/CT-CCHN",
|
||||
"title": "BS CKII"
|
||||
},
|
||||
{
|
||||
"doctorId": "ad4b09f7-ff6b-4196-9fb0-c6663662ab8b",
|
||||
"doctorCode": "00822",
|
||||
"fullName": "BS. MẠCH CHÍ QUYỀN",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "005349/CT-CCHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "6651ac6b-54fd-4dd5-a744-451fb86eb609",
|
||||
"doctorCode": "01160",
|
||||
"fullName": "THS. BS. ĐẶNG VĂN SÔ ĐA",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "0002970/ST-CCHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "c8261458-452a-4a8c-a32c-805c5a6b2bc6",
|
||||
"doctorCode": "00567",
|
||||
"fullName": "BS CKI. TRẦN VĂN LẢM",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "007679/CT-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "a4d2701a-99eb-4f92-ac01-f45b3578e142",
|
||||
"doctorCode": "00935",
|
||||
"fullName": "BS CKI. TRẦN HOÀNG ÂN",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "004641/CT-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "ec517303-c40f-473f-8eb6-09f54676bcc3",
|
||||
"doctorCode": "00530",
|
||||
"fullName": "BS. PHAN THỊ HỒNG LẠC",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "007401/CT-CCHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "20f640db-d100-4a29-b5a7-8f44391b94fc",
|
||||
"doctorCode": "00529",
|
||||
"fullName": "THS. BS. LƯU ĐẶNG DIỄM TRÂN",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "007402/CT-CCHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "b2e0dd81-c355-42da-8bd2-70f9ac82c0e4",
|
||||
"doctorCode": "01070",
|
||||
"fullName": "BS. HUỲNH LINH TÂM",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "05127/ST-CCHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "d3c407ec-6b26-4628-9211-ec5675c20188",
|
||||
"doctorCode": "01076",
|
||||
"fullName": "THS. BS. MAI PHƯƠNG THẢO",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "006858/KG-CCHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "761f92fa-2955-4943-9283-d8b569cdcce3",
|
||||
"doctorCode": "00632",
|
||||
"fullName": "BS. MAI HOÀNG DIL",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "008087/CT-CCHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "2a548de1-9f68-47d2-8839-1298c1e98189",
|
||||
"doctorCode": "00638",
|
||||
"fullName": "BS. LÊ VÂN NHI",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "008077/CT-CCHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "0b49c1cb-914d-4f72-968b-8f383f27cf3a",
|
||||
"doctorCode": "00621",
|
||||
"fullName": "BS. PHAN THANH THẾ",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "008091/CT-CCHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "14a384a0-211b-463a-8c4f-5e26319c50b2",
|
||||
"doctorCode": "01049",
|
||||
"fullName": "BS. LÊ LÂM TUYẾT DUY",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000315/VL-CCHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "7abeabe5-ddb0-4ac3-b4b1-3fbb3cb1cf29",
|
||||
"doctorCode": "00973",
|
||||
"fullName": "THS. BS. LÊ THỊ CHI LAN",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "007884/CT-CCHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "f9bca81b-329b-49d9-8ca1-4dcd662e0fc0",
|
||||
"doctorCode": "01039",
|
||||
"fullName": "BS CKI. NGUYỄN ĐỨC CHỈNH",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "0024474/BYT-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "729f966c-af07-45e3-b690-845b81c40639",
|
||||
"doctorCode": "00629",
|
||||
"fullName": "BS. NGUYỄN HUỲNH ĐÀO",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "008078/CT-CCHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "710f9574-7266-4241-ad0a-11918f74b7c7",
|
||||
"doctorCode": "00875",
|
||||
"fullName": "BS CKI. TẠ MỸ NGỌC",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "0026097/BYT-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "b9c3769b-46c0-4fd0-9731-89b1e7564cc5",
|
||||
"doctorCode": "00930",
|
||||
"fullName": "BS. ĐẶNG THỊ NHƯ MAI",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "07870/AG-CCHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "ab905624-a7c1-483e-b924-afa80dfed2e9",
|
||||
"doctorCode": "00797",
|
||||
"fullName": "THS. BS. NGUYỄN THỊ PHƯƠNG ANH",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "003347/HAUG-CCHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "bdb7e9ad-eaef-47a0-a767-0d45da31e620",
|
||||
"doctorCode": "00832",
|
||||
"fullName": "BS. NGÔ MINH TRƯỜNG",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "006904/CT-CCHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "29db7794-f91b-495b-be01-3fd2132da084",
|
||||
"doctorCode": "00824",
|
||||
"fullName": "BS. TRẦN QUỐC THÁI",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "004923/CT-CCHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "6292c6f9-35e0-4d86-a0fb-2612abe5851e",
|
||||
"doctorCode": "00847",
|
||||
"fullName": "BS. NGUYỄN ANH MỸ",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "046062/BYT-CCHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "cd7cb26f-901f-4159-8255-7a710cd5bda4",
|
||||
"doctorCode": "00894",
|
||||
"fullName": "BS CKI. NGUYỄN NHỰT THÁI",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000232/CT-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "3e58f86f-2f85-421c-af30-40e5cca0a5cc",
|
||||
"doctorCode": "01201",
|
||||
"fullName": "BS CKI. NGUYỄN DƯƠNG KHANH",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "004757/CM-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "23225383-b10b-4d57-b11f-0768165f9b61",
|
||||
"doctorCode": "01206",
|
||||
"fullName": "BS. PHẠM CÔNG ĐỊNH",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "007789/CT-CCHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "20ab7d4d-3db4-44ed-aef1-79b9057880db",
|
||||
"doctorCode": "01202",
|
||||
"fullName": "BS CKI. DIỆP THỊ LÊ",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "005366/CT-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "d63acf14-3613-4d61-bff9-01acded94d75",
|
||||
"doctorCode": "01228",
|
||||
"fullName": "BS CKI. TRẦN NGUYỄN KHÁNH",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "0018792/HCM-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "793da9e5-c268-47a2-9aa4-8816ab8efefc",
|
||||
"doctorCode": "01229",
|
||||
"fullName": "BS CKI. NGUYỄN THỊ NGỌC TUYỀN",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "003198/HAUG-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "5bf35f60-d087-459d-9b32-3723cb84ff46",
|
||||
"doctorCode": "00801",
|
||||
"fullName": "BS. ĐỖ ĐỨC THẮNG",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000162/CT-GPHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "a45f5094-0d06-48fd-9f49-f5518385cdcb",
|
||||
"doctorCode": "01091",
|
||||
"fullName": "THS. BS. DIỆP TIẾN ĐẠT",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000120/CT-GPHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "813e5ecf-16a4-4cd4-820b-ec38ebf4b60b",
|
||||
"doctorCode": "01236",
|
||||
"fullName": "BS. NGUYỄN MINH MẨN",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000072/BTR-GPHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "7b71ea60-189a-4fdb-bd94-aefdcb03f7c0",
|
||||
"doctorCode": "00344",
|
||||
"fullName": "BS CKII. NGUYỄN MINH NGUYỆT",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000125/CT-GPHN",
|
||||
"title": "BS CKII"
|
||||
},
|
||||
{
|
||||
"doctorId": "46d6494d-67be-43ad-ba74-b8c666dddac3",
|
||||
"doctorCode": "01174",
|
||||
"fullName": "BS CKI. TRẦN ĐẮC ĐỨC",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "007053/CT-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "bbc9dfb7-0804-490a-ab48-076245a649b7",
|
||||
"doctorCode": "01358",
|
||||
"fullName": "BS CKI. DƯƠNG Ý NHI",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "005408/ST-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "8808c33c-3437-441e-aed3-7654c6558307",
|
||||
"doctorCode": "01369",
|
||||
"fullName": "BS CKI. TRƯƠNG DUY ĐĂNG",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "049072/BYT-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "0a725e2e-6ee2-4622-90e6-b4677bb0de68",
|
||||
"doctorCode": "01291",
|
||||
"fullName": "BS. ĐỖ HỮU NGHĨA",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000001/HAUG-GPHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "e3d89436-7d6c-4ce9-bd2d-614189554451",
|
||||
"doctorCode": "01443",
|
||||
"fullName": "THS. BS. NGUYỄN BẢO THUY",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000569/KG-GPHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "0974a7bf-aa9a-4f10-a811-cd9c8fe7f0dc",
|
||||
"doctorCode": "01432",
|
||||
"fullName": "THS. BS. TRẦN NGUYỄN THẢO LIÊN",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000696/TG-GPHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "05888715-3031-4026-b318-62ef26a2e2b1",
|
||||
"doctorCode": "01509",
|
||||
"fullName": "BS CKII. PHAN THỊ THU NGÂN",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "001118/CT-CCHN",
|
||||
"title": "BS CKII"
|
||||
},
|
||||
{
|
||||
"doctorId": "d68ba1ab-9d65-41c2-bc7e-236cfa571f5f",
|
||||
"doctorCode": "HT0041",
|
||||
"fullName": "THS. BS. ĐẶNG LÊ TRANG NGUYÊN",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "918a6b31-fa6c-4a34-8273-7d2f6266f615",
|
||||
"doctorCode": "00202",
|
||||
"fullName": "BS CKI. NGUYỄN HỮU VỊNH",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "029656/BYT-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "52775722-abf2-4b63-b3b3-320700660eab",
|
||||
"doctorCode": "01431",
|
||||
"fullName": "THS. BS. LÊ XUÂN TRIỆU",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "003074/HAUG-CCHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "35cc2469-440f-4649-85e3-6f547c57d38a",
|
||||
"doctorCode": "01391",
|
||||
"fullName": "THS. BS. TRIỆU QUANG THÁI",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "007139/CT-CCHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "27bb8653-2d6f-4d30-963a-bfc512f8250e",
|
||||
"doctorCode": "01351",
|
||||
"fullName": "BS. LÊ THỊ TRANG THẢO",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000282/CT-GPHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "7757d2b8-cbde-47ab-9a4d-696ed46bc6b4",
|
||||
"doctorCode": "01442",
|
||||
"fullName": "THS. BS. THẠCH VĂN TÙNG",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "004025/TV-CCHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "32b89244-660d-4c04-9219-20ae7d6fec63",
|
||||
"doctorCode": "01313",
|
||||
"fullName": "BS CKI. NGUYỄN HỮU BÚT",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000433/CT-GPHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "cd12c72c-aad6-440e-b1ff-8db3aa530ba6",
|
||||
"doctorCode": "01191",
|
||||
"fullName": "THS. BS. BÙI MINH HIẾU",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000531/CT-GPHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "22340179-5fda-4b57-9e29-95acdfbd5f5b",
|
||||
"doctorCode": "01459",
|
||||
"fullName": "BS CKI. HUỲNH THỊ NHẬT BÌNH",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000364/HAUG-GPHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "71da0437-2d35-4176-be9e-8e59fe1c0b2f",
|
||||
"doctorCode": "00948",
|
||||
"fullName": "THS. BS. NGUYỄN NGỌC ANH THƯ",
|
||||
"birthYear": 1986,
|
||||
"gender": null,
|
||||
"practicingCertificate": "001796/CT-GPHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "72079a55-61cd-4ad3-a1ba-a79b498d32e9",
|
||||
"doctorCode": "00819",
|
||||
"fullName": "BS CKI. LÊ TẤN AN",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "05126/ST-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "285583fa-39cb-4b50-9d60-8a00316b7a58",
|
||||
"doctorCode": "00752",
|
||||
"fullName": "BS CKI. NGUYỄN THANH LIÊM",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "003227/HAUG-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "304fed90-94e9-48b2-bb2c-1faac4fcf437",
|
||||
"doctorCode": "01438",
|
||||
"fullName": "BS CKI. NGUYỄN ĐOÀN TRỌNG NHÂN",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "053518/HCM-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "6beecf09-9c9b-4d27-99cc-bf26e904c8a3",
|
||||
"doctorCode": "00402",
|
||||
"fullName": "BS CKI. DƯƠNG HOÀNG LINH",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "006369/KG-CCHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "3a1d3bef-01fc-1a5c-f05d-ad4a4f38495c",
|
||||
"doctorCode": "01536",
|
||||
"fullName": "THS. BS. ĐOÀN TÚ",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000412/BTR-GPHN",
|
||||
"title": "THS. BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "3a1d3bf1-467e-2dcd-a3c1-c43d5bc8cfd8",
|
||||
"doctorCode": "01073",
|
||||
"fullName": "BS. LÊ VĂN AN",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000958/CT-GPHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "3a1d3bf1-87c7-5b2e-3115-0d6c681ce3c3",
|
||||
"doctorCode": "01472",
|
||||
"fullName": "BS CKI. BÙI NHƯ QUỲNH",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000584/CM-GPHN",
|
||||
"title": "BS CKI"
|
||||
},
|
||||
{
|
||||
"doctorId": "3a1d3c02-e2e3-16af-00f3-1d0dc0ba07fb",
|
||||
"doctorCode": "01505",
|
||||
"fullName": "BS. TRẦN VĂN TRƯỜNG",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "0004775/VL-CCHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "3a1e7f85-818b-ca73-210e-0e9b6e98b65d",
|
||||
"doctorCode": "01074",
|
||||
"fullName": "BS. NGUYỄN THỊ MỸ HẠNH",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000959/CT-GPHN",
|
||||
"title": "BS"
|
||||
},
|
||||
{
|
||||
"doctorId": "3a1f67ea-6bcf-243c-999e-7e9bbb09a18e",
|
||||
"doctorCode": "01623",
|
||||
"fullName": "THS. BS. NGUYỄN THIÊN THẠCH",
|
||||
"birthYear": 1999,
|
||||
"gender": null,
|
||||
"practicingCertificate": "001521/CT-GPHN",
|
||||
"title": "THS. BS."
|
||||
},
|
||||
{
|
||||
"doctorId": "3a1f67f8-f100-5355-7fc0-dd763b444695",
|
||||
"doctorCode": "01511",
|
||||
"fullName": "BS CKI. TRƯƠNG DƯƠNG HƯNG",
|
||||
"birthYear": 1998,
|
||||
"gender": null,
|
||||
"practicingCertificate": "001538/CT-GPHN",
|
||||
"title": "BS CKI."
|
||||
},
|
||||
{
|
||||
"doctorId": "3a1f67fb-d0bc-c719-782f-879b9162678d",
|
||||
"doctorCode": "01554",
|
||||
"fullName": "BS CKI. THÁI THỊ MINH THƯ",
|
||||
"birthYear": 1998,
|
||||
"gender": null,
|
||||
"practicingCertificate": "001539/CT-GPHN",
|
||||
"title": ""
|
||||
},
|
||||
{
|
||||
"doctorId": "3a1ff878-77cc-f022-09e3-7d02fee7b8d0",
|
||||
"doctorCode": "01684",
|
||||
"fullName": "THS. BS. NGUYỄN HUỲNH THIỆN DUYÊN",
|
||||
"birthYear": 1998,
|
||||
"gender": null,
|
||||
"practicingCertificate": "001479/AG-GPHN",
|
||||
"title": "THS. BS. "
|
||||
},
|
||||
{
|
||||
"doctorId": "3a203fb7-ab1b-264d-f354-d57b8ad7453f",
|
||||
"doctorCode": "01713",
|
||||
"fullName": "BS CKII. DI VĂN ĐUA",
|
||||
"birthYear": 1978,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000098/CM-CCHN",
|
||||
"title": "BS CKII. "
|
||||
},
|
||||
{
|
||||
"doctorId": "3a20e599-29c8-30d3-6cb3-3a0e609e30b0",
|
||||
"doctorCode": "01669",
|
||||
"fullName": "BS CKI. HỒ NGỌC THIỆN",
|
||||
"birthYear": 1998,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000756/QNG-GPHN",
|
||||
"title": "BS CKI."
|
||||
},
|
||||
{
|
||||
"doctorId": "3a20e599-bbd9-8228-e321-4e9c82cee5df",
|
||||
"doctorCode": "01670",
|
||||
"fullName": "BS. LÊ NGUYỄN NHẬT MINH",
|
||||
"birthYear": 1999,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000934/CM-GPHN",
|
||||
"title": "BS."
|
||||
},
|
||||
{
|
||||
"doctorId": "3a20e59a-d030-51ff-f10c-0b6938fcbca2",
|
||||
"doctorCode": "01672",
|
||||
"fullName": "THS. BS. NGUYỄN NHÂN",
|
||||
"birthYear": 1994,
|
||||
"gender": null,
|
||||
"practicingCertificate": "001417/AG-GPHN",
|
||||
"title": "THS. BS."
|
||||
},
|
||||
{
|
||||
"doctorId": "3a20e59b-8323-5e11-4733-b9f9b1455151",
|
||||
"doctorCode": "01686",
|
||||
"fullName": "BS. LÂM QUỐC ĐẠT",
|
||||
"birthYear": 1998,
|
||||
"gender": null,
|
||||
"practicingCertificate": "000096/TV-GPHN",
|
||||
"title": "BS."
|
||||
},
|
||||
{
|
||||
"doctorId": "3a219325-6bbb-1b37-8cca-372fb296c385",
|
||||
"doctorCode": "00995",
|
||||
"fullName": "THS. BS. HUỲNH QUỐC CƯỜNG",
|
||||
"birthYear": null,
|
||||
"gender": null,
|
||||
"practicingCertificate": "002229/CT-GPHN",
|
||||
"title": "THS. BS. "
|
||||
},
|
||||
{
|
||||
"doctorId": "3a2219d7-104f-a235-6677-125e64cf03fe",
|
||||
"doctorCode": "01753",
|
||||
"fullName": "BS CKI. TRƯƠNG MỸ ÁI",
|
||||
"birthYear": 1995,
|
||||
"gender": null,
|
||||
"practicingCertificate": "007006/CT-CCHN",
|
||||
"title": "BS CKI. "
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true,
|
||||
"statusCode": 200,
|
||||
"timestamp": "2026-07-22T14:16:07.9715182+07:00"
|
||||
},
|
||||
"doctor_00492_schedule": {
|
||||
"data": {
|
||||
"doctorId": "01164889-8323-4036-8a31-486a12b74297",
|
||||
"doctorCode": "00492",
|
||||
"fromDate": "2026-07-22",
|
||||
"toDate": "2026-08-05",
|
||||
"days": [
|
||||
{
|
||||
"date": "2026-07-22",
|
||||
"dayOfWeek": 4
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true,
|
||||
"statusCode": 200,
|
||||
"timestamp": "2026-07-22T14:16:08.2360549+07:00"
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,7 @@ class HtmlSnippetTool {
|
||||
this.wrapper.style.width = '100%';
|
||||
this.wrapper.style.boxSizing = 'border-box';
|
||||
|
||||
if (this.data.id) {
|
||||
this._showPreview(this.data.id);
|
||||
} else {
|
||||
this._showInput();
|
||||
}
|
||||
this._showInput();
|
||||
|
||||
return this.wrapper;
|
||||
}
|
||||
@@ -43,7 +39,9 @@ class HtmlSnippetTool {
|
||||
title.style.marginTop = '0';
|
||||
title.style.marginBottom = '10px';
|
||||
title.style.padding = '15px 15px 0 15px';
|
||||
title.innerText = 'Insert Predefined HTML Snippet';
|
||||
title.innerText = 'HTML Snippet ID:';
|
||||
title.style.fontSize = '14px';
|
||||
title.style.color = '#333';
|
||||
|
||||
const inputContainer = document.createElement('div');
|
||||
inputContainer.style.display = 'flex';
|
||||
@@ -55,160 +53,16 @@ class HtmlSnippetTool {
|
||||
input.placeholder = 'Enter Snippet ID (e.g., test_banner)';
|
||||
input.value = this.data.id;
|
||||
|
||||
const loadBtn = document.createElement('button');
|
||||
loadBtn.innerText = 'Load Preview';
|
||||
loadBtn.style.padding = '5px 15px';
|
||||
loadBtn.style.cursor = 'pointer';
|
||||
|
||||
loadBtn.addEventListener('click', () => {
|
||||
if (input.value.trim()) {
|
||||
this._showPreview(input.value.trim());
|
||||
}
|
||||
input.addEventListener('input', (e) => {
|
||||
this.data.id = e.target.value.trim();
|
||||
});
|
||||
|
||||
inputContainer.appendChild(input);
|
||||
inputContainer.appendChild(loadBtn);
|
||||
|
||||
this.wrapper.appendChild(title);
|
||||
this.wrapper.appendChild(inputContainer);
|
||||
}
|
||||
|
||||
_showPreview(snippetId) {
|
||||
this.wrapper.innerHTML = '';
|
||||
|
||||
// Header bar with snippet name and Edit ID button
|
||||
const header = document.createElement('div');
|
||||
header.style.display = 'flex';
|
||||
header.style.justifyContent = 'space-between';
|
||||
header.style.alignItems = 'center';
|
||||
header.style.padding = '8px 15px';
|
||||
header.style.borderBottom = '1px solid #ddd';
|
||||
header.style.background = '#f0f0f0';
|
||||
header.style.borderRadius = '5px 5px 0 0';
|
||||
|
||||
const title = document.createElement('strong');
|
||||
title.innerText = 'Snippet: ' + snippetId;
|
||||
title.style.fontSize = '13px';
|
||||
title.style.color = '#555';
|
||||
|
||||
const editBtn = document.createElement('button');
|
||||
editBtn.innerText = 'Edit ID';
|
||||
editBtn.style.fontSize = '12px';
|
||||
editBtn.style.cursor = 'pointer';
|
||||
editBtn.style.padding = '2px 8px';
|
||||
editBtn.style.border = '1px solid #ccc';
|
||||
editBtn.style.borderRadius = '3px';
|
||||
editBtn.style.background = '#fff';
|
||||
editBtn.addEventListener('click', () => {
|
||||
this._showInput();
|
||||
});
|
||||
|
||||
header.appendChild(title);
|
||||
header.appendChild(editBtn);
|
||||
this.wrapper.appendChild(header);
|
||||
|
||||
// Loading indicator
|
||||
const loadingDiv = document.createElement('div');
|
||||
loadingDiv.style.padding = '20px';
|
||||
loadingDiv.style.color = '#666';
|
||||
loadingDiv.style.textAlign = 'center';
|
||||
loadingDiv.innerText = 'Loading preview...';
|
||||
this.wrapper.appendChild(loadingDiv);
|
||||
|
||||
// Fetch the snippet HTML
|
||||
fetch('/api/manage/snippets/' + encodeURIComponent(snippetId))
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('Snippet not found');
|
||||
return response.text();
|
||||
})
|
||||
.then(html => {
|
||||
this.data.id = snippetId;
|
||||
|
||||
// Remove loading indicator
|
||||
loadingDiv.remove();
|
||||
|
||||
// Create an iframe for isolated WYSIWYG rendering
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.style.width = '100%';
|
||||
iframe.style.border = 'none';
|
||||
iframe.style.display = 'block';
|
||||
iframe.style.borderRadius = '0 0 5px 5px';
|
||||
iframe.style.minHeight = '80px';
|
||||
// Scrolling is disabled; height auto-adjusts
|
||||
iframe.scrolling = 'no';
|
||||
|
||||
this.wrapper.appendChild(iframe);
|
||||
|
||||
// Write the full HTML document into the iframe, including theme CSS
|
||||
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
iframeDoc.open();
|
||||
iframeDoc.write(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400..700;1,400..700&family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/css/umass.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
/* Prevent any internal links from being clickable */
|
||||
a { pointer-events: none; }
|
||||
/* Ensure images/videos scale properly */
|
||||
img, video, iframe { max-width: 100%; height: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${html}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
iframeDoc.close();
|
||||
|
||||
// Auto-resize iframe to fit content
|
||||
const resizeIframe = () => {
|
||||
try {
|
||||
const body = iframeDoc.body;
|
||||
const docEl = iframeDoc.documentElement;
|
||||
const height = Math.max(
|
||||
body.scrollHeight, body.offsetHeight,
|
||||
docEl.clientHeight, docEl.scrollHeight, docEl.offsetHeight
|
||||
);
|
||||
iframe.style.height = height + 'px';
|
||||
} catch (e) {
|
||||
iframe.style.height = '300px';
|
||||
}
|
||||
};
|
||||
|
||||
// Resize after load and after images load
|
||||
iframe.addEventListener('load', resizeIframe);
|
||||
setTimeout(resizeIframe, 500);
|
||||
setTimeout(resizeIframe, 1500);
|
||||
setTimeout(resizeIframe, 3000);
|
||||
})
|
||||
.catch(error => {
|
||||
loadingDiv.remove();
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.style.padding = '15px';
|
||||
errorDiv.innerHTML = '<div style="color: red;">Error: ' + error.message + '</div>';
|
||||
const backBtn = document.createElement('button');
|
||||
backBtn.innerText = 'Try Again';
|
||||
backBtn.style.marginTop = '10px';
|
||||
backBtn.style.cursor = 'pointer';
|
||||
backBtn.addEventListener('click', () => this._showInput());
|
||||
errorDiv.appendChild(backBtn);
|
||||
this.wrapper.appendChild(errorDiv);
|
||||
});
|
||||
}
|
||||
|
||||
save(blockContent) {
|
||||
return {
|
||||
id: this.data.id
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{fragments/layout}">
|
||||
layout:decorate="~{themes/__${activeTheme}__/layout(forceFullWidth=true, bodyClass='doctor-page')}">
|
||||
|
||||
<head>
|
||||
<th:block layout:fragment="head">
|
||||
@@ -137,7 +137,7 @@
|
||||
const createButton = (content, onClick, disabled, isActive) => {
|
||||
const btn = document.createElement('button');
|
||||
if (isActive) {
|
||||
btn.className = 'lg:size-10 md:size-9 size-8 flex items-center justify-center rounded-full border body-1 cursor-pointer bg-primary-600 text-white border-primary-600';
|
||||
btn.className = 'lg:size-10 md:size-9 size-8 flex items-center justify-center rounded-full border body-1 cursor-pointer bg-primary-600 text-white border-primary-600 is-active';
|
||||
} else {
|
||||
btn.className = 'lg:size-10 md:size-9 size-8 flex items-center justify-center rounded-full border border-gray-300 bg-white text-black disabled:opacity-50 disabled:!bg-white disabled:!text-black cursor-pointer lg:hover:bg-primary-600 lg:hover:text-white lg:duration-150 transition-colors';
|
||||
}
|
||||
|
||||
@@ -360,7 +360,7 @@
|
||||
</div>
|
||||
<div class="links-container">
|
||||
<div class="f--field f--button">
|
||||
<a href="https://sisvietnam.vn/admissions/apply"
|
||||
<a href="https://sisvietnam.vn/admissions/apply"
|
||||
class="button-text-link button-context-light " aria-label="Apply" target="_self"
|
||||
data-component-id="umass_base:button">
|
||||
<span class="button-text">
|
||||
|
||||
@@ -125,6 +125,15 @@
|
||||
<input type="text" class="form-control form-control-sm" id="filterKeyword" name="keyword"
|
||||
placeholder="Filename..." th:value="${keyword}">
|
||||
</div>
|
||||
<div class="form-group mr-3 mb-2">
|
||||
<label for="filterSize" class="mr-2 font-weight-bold">Per Page:</label>
|
||||
<select class="form-control form-control-sm" id="filterSize" name="size" onchange="this.form.submit()">
|
||||
<option value="10" th:selected="${pageSize == 10}">10</option>
|
||||
<option value="20" th:selected="${pageSize == 20}">20</option>
|
||||
<option value="50" th:selected="${pageSize == 50}">50</option>
|
||||
<option value="100" th:selected="${pageSize == 100}">100</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-sm btn-primary mb-2 mr-2">
|
||||
<i class="fas fa-search"></i> Filter
|
||||
</button>
|
||||
@@ -137,11 +146,20 @@
|
||||
|
||||
<!-- Media Grid -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<div class="card-header py-3 d-flex align-items-center justify-content-between">
|
||||
<h6 class="m-0 font-weight-bold text-primary">
|
||||
All Media
|
||||
<span class="badge badge-light ml-1" th:text="${#lists.size(mediaList)}"></span>
|
||||
<span class="badge badge-light ml-1" th:text="${mediaPage.totalElements}"></span>
|
||||
</h6>
|
||||
<div th:unless="${#lists.isEmpty(mediaList)}">
|
||||
<div class="custom-control custom-checkbox d-inline-block mr-3">
|
||||
<input type="checkbox" class="custom-control-input" id="selectAllMedia">
|
||||
<label class="custom-control-label" for="selectAllMedia">Select All</label>
|
||||
</div>
|
||||
<button type="submit" form="bulkDeleteForm" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to delete all selected media?');">
|
||||
<i class="fas fa-trash"></i> Delete Selected
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Empty state -->
|
||||
@@ -152,14 +170,20 @@
|
||||
</div>
|
||||
|
||||
<!-- Grid -->
|
||||
<div th:unless="${#lists.isEmpty(mediaList)}" class="media-grid">
|
||||
<div th:each="media : ${mediaList}" class="media-card">
|
||||
<form th:unless="${#lists.isEmpty(mediaList)}" th:action="@{/manage/media/bulk-delete}" method="post" id="bulkDeleteForm">
|
||||
<div class="media-grid">
|
||||
<div th:each="media : ${mediaList}" class="media-card position-relative">
|
||||
<!-- Bulk Select Checkbox -->
|
||||
<div class="position-absolute" style="top: 8px; right: 8px; z-index: 10;">
|
||||
<input type="checkbox" name="mediaIds" th:value="${media.id}" class="media-checkbox" style="transform: scale(1.5);">
|
||||
</div>
|
||||
<a th:href="@{/manage/media/{id}(id=${media.id})}" style="text-decoration:none; color:inherit;">
|
||||
<div class="media-thumb">
|
||||
<!-- Image thumbnail -->
|
||||
<img th:if="${media.mediaType.name() == 'IMAGE'}"
|
||||
th:src="${media.fileUrl}"
|
||||
th:alt="${media.altText ?: media.originalFilename}">
|
||||
th:alt="${media.altText ?: media.originalFilename}"
|
||||
loading="lazy">
|
||||
<!-- Document icon -->
|
||||
<i th:if="${media.mediaType.name() == 'DOCUMENT'}" class="fas fa-file-alt file-icon doc"></i>
|
||||
<!-- Video icon -->
|
||||
@@ -196,9 +220,73 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div th:if="${mediaPage.totalPages > 1}" class="mt-4">
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item" th:classappend="${mediaPage.first} ? 'disabled'">
|
||||
<a class="page-link" th:href="@{/manage/media(page=${mediaPage.number - 1}, size=${pageSize}, type=${selectedType}, keyword=${keyword})}" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Display all pages if less than 10 pages, otherwise simple prev/next is better, or use sequence -->
|
||||
<li class="page-item" th:each="i : ${#numbers.sequence(0, mediaPage.totalPages - 1)}"
|
||||
th:classappend="${mediaPage.number == i} ? 'active'"
|
||||
th:if="${i >= mediaPage.number - 3 and i <= mediaPage.number + 3}">
|
||||
<a class="page-link" th:href="@{/manage/media(page=${i}, size=${pageSize}, type=${selectedType}, keyword=${keyword})}" th:text="${i + 1}">1</a>
|
||||
</li>
|
||||
|
||||
<li class="page-item" th:classappend="${mediaPage.last} ? 'disabled'">
|
||||
<a class="page-link" th:href="@{/manage/media(page=${mediaPage.number + 1}, size=${pageSize}, type=${selectedType}, keyword=${keyword})}" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var selectAll = document.getElementById('selectAllMedia');
|
||||
var checkboxes = Array.from(document.querySelectorAll('.media-checkbox'));
|
||||
var lastChecked = null;
|
||||
|
||||
if (selectAll) {
|
||||
selectAll.addEventListener('change', function() {
|
||||
checkboxes.forEach(function(cb) {
|
||||
cb.checked = selectAll.checked;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
checkboxes.forEach(function(checkbox) {
|
||||
checkbox.addEventListener('click', function(e) {
|
||||
if (!lastChecked) {
|
||||
lastChecked = this;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.shiftKey) {
|
||||
var start = checkboxes.indexOf(this);
|
||||
var end = checkboxes.indexOf(lastChecked);
|
||||
var slice = checkboxes.slice(Math.min(start, end), Math.max(start, end) + 1);
|
||||
|
||||
slice.forEach(function(cb) {
|
||||
cb.checked = lastChecked.checked;
|
||||
});
|
||||
}
|
||||
|
||||
lastChecked = this;
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{themes/__${activeTheme}__/layout}">
|
||||
|
||||
<head>
|
||||
<title th:text="${page.title}">Page Title</title>
|
||||
<title th:text="${page.title}">Bệnh Viện S.I.S Cần Thơ</title>
|
||||
<!-- Add Meta Description for SEO -->
|
||||
<meta name="description" th:if="${page.metaDescription != null}" th:content="${page.metaDescription}" />
|
||||
|
||||
|
||||
@@ -151,11 +151,20 @@
|
||||
flex: 1;
|
||||
}
|
||||
.mega-col-info h3 {
|
||||
color: var(--color-brand);
|
||||
color: #111827;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.mega-col-info h3 .highlight {
|
||||
color: #881c1c;
|
||||
display: block;
|
||||
}
|
||||
.mega-col-info h3 span {
|
||||
color: #881c1c;
|
||||
display: block;
|
||||
}
|
||||
.mega-col-info p {
|
||||
color: var(--color-black);
|
||||
@@ -313,7 +322,7 @@
|
||||
</style>
|
||||
<header id="l--main-header">
|
||||
<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" style="background: linear-gradient(0deg, transparent, #000) !important;">
|
||||
<div class="region region-header r--region r--header">
|
||||
<section data-component-id="umass_base:site-header" aria-label="Site Header">
|
||||
<div class="header-left" style="display: flex; flex-direction: column; align-items: center; width: max-content;">
|
||||
<div data-component-id="umass_base:header-branding">
|
||||
@@ -354,7 +363,7 @@
|
||||
<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>
|
||||
</svg> Back </button>
|
||||
<span class="navigation-title" th:text="${not #strings.isEmpty(item.title) ? item.title : item.label}">Menu Item</span>
|
||||
<span class="navigation-title" th:utext="${not #strings.isEmpty(item.title) ? item.title : item.label}">Menu Item</span>
|
||||
|
||||
<!-- Mobile Submenu Layout (Simple List) -->
|
||||
<ul class="submenu mobile-only">
|
||||
@@ -367,7 +376,7 @@
|
||||
<div class="mega-menu-content desktop-only">
|
||||
<!-- Column 1: Info (Title and Text) -->
|
||||
<div class="mega-col-info">
|
||||
<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>
|
||||
<h3 th:utext="${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 Đ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 != '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>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<!-- wp_head hook -->
|
||||
<th:block th:utext="${hookManager.doActionAndReturn('wp_head')}"></th:block>
|
||||
</head>
|
||||
<body class="umass-platform-homepage path-frontpage page-node-type-homepage homepage transparent-header">
|
||||
<body th:class="${(bodyClass != null ? bodyClass : '') + ((page != null and page.pageType != null and page.pageType.name() == 'HOME') ? ' umass-platform-homepage path-frontpage page-node-type-homepage homepage transparent-header' : '')}">
|
||||
<div class="dialog-off-canvas-main-canvas" data-off-canvas-main-canvas="">
|
||||
<!-- Include Header Fragment -->
|
||||
<header th:replace="~{themes/__${activeTheme}__/header :: header}"></header>
|
||||
@@ -29,17 +29,15 @@
|
||||
<div class="content">
|
||||
<div class="region region-content r--region r--content">
|
||||
<!-- Content Area -->
|
||||
<div class="responsive-flex-container" th:with="isFullWidth=${(page != null and page.layout != null and page.layout.name() == 'FULL_WIDTH') or (post != null and post.layout != null and post.layout.name() == 'FULL_WIDTH')}" th:style="${isFullWidth} ? 'min-height: 400px;' : 'display: flex; min-height: 400px; padding: 20px; max-width: 1400px; margin: 0 auto;'">
|
||||
<div class="responsive-flex-content" th:style="${isFullWidth} ? 'width: 100%;' : 'flex: 3; padding-right: 20px;'" layout:fragment="content"></div>
|
||||
<aside class="widget-sidebar-area" th:unless="${isFullWidth}" style="flex: 1; padding: 15px; border-radius: 5px;">
|
||||
<th:block th:if="${sidebarWidgets != null and !sidebarWidgets.empty}">
|
||||
<h4>Sidebar</h4>
|
||||
<div th:each="widget : ${sidebarWidgets}" style="margin-bottom: 20px;">
|
||||
<h5 th:text="${widget.title}" style="border-bottom: 1px solid #ddd; padding-bottom: 5px;">Widget Title</h5>
|
||||
<div th:if="${widget.type == 'HTML'}" th:utext="${widget.content}"></div>
|
||||
<div th:if="${widget.type == 'TEXT'}" th:text="${widget.content}"></div>
|
||||
</div>
|
||||
</th:block>
|
||||
<div class="responsive-flex-container" th:with="isFullWidth=${(forceFullWidth != null and forceFullWidth) or (page != null and page.layout != null and page.layout.name() == 'FULL_WIDTH') or (post != null and post.layout != null and post.layout.name() == 'FULL_WIDTH')}, hasWidgets=${sidebarWidgets != null and !sidebarWidgets.empty}, showSidebar=${!isFullWidth and hasWidgets}" th:style="${isFullWidth} ? 'min-height: 400px;' : 'display: flex; min-height: 400px; padding: 20px; max-width: 1400px; margin: 0 auto;'">
|
||||
<div class="responsive-flex-content" th:style="${showSidebar} ? 'flex: 3; padding-right: 20px;' : 'width: 100%;'" layout:fragment="content"></div>
|
||||
<aside class="widget-sidebar-area" th:if="${showSidebar}" style="flex: 1; padding: 15px; border-radius: 5px;">
|
||||
<h4>Sidebar</h4>
|
||||
<div th:each="widget : ${sidebarWidgets}" style="margin-bottom: 20px;">
|
||||
<h5 th:text="${widget.title}" style="border-bottom: 1px solid #ddd; padding-bottom: 5px;">Widget Title</h5>
|
||||
<div th:if="${widget.type == 'HTML'}" th:utext="${widget.content}"></div>
|
||||
<div th:if="${widget.type == 'TEXT'}" th:text="${widget.content}"></div>
|
||||
</div>
|
||||
</aside>
|
||||
<style>
|
||||
@media (max-width: 768px) {
|
||||
|
||||
|
After Width: | Height: | Size: 8.2 MiB |
|
After Width: | Height: | Size: 763 KiB |
|
After Width: | Height: | Size: 833 KiB |
|
After Width: | Height: | Size: 879 KiB |
|
After Width: | Height: | Size: 764 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 624 KiB |
|
After Width: | Height: | Size: 786 KiB |
|
After Width: | Height: | Size: 848 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 784 KiB |
|
After Width: | Height: | Size: 804 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 32 MiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 866 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 938 KiB |
|
After Width: | Height: | Size: 740 KiB |
|
After Width: | Height: | Size: 784 KiB |
|
After Width: | Height: | Size: 783 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 455 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 817 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 672 KiB |
|
After Width: | Height: | Size: 599 KiB |
|
After Width: | Height: | Size: 908 KiB |
|
After Width: | Height: | Size: 712 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 800 KiB |
|
After Width: | Height: | Size: 752 KiB |
|
After Width: | Height: | Size: 725 KiB |
|
After Width: | Height: | Size: 776 KiB |
|
After Width: | Height: | Size: 782 KiB |
|
After Width: | Height: | Size: 641 KiB |
|
After Width: | Height: | Size: 794 KiB |
|
After Width: | Height: | Size: 897 KiB |
|
After Width: | Height: | Size: 595 KiB |
|
After Width: | Height: | Size: 766 KiB |
|
After Width: | Height: | Size: 916 KiB |
|
After Width: | Height: | Size: 661 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 459 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 620 KiB |
|
After Width: | Height: | Size: 503 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 9.1 MiB |
|
After Width: | Height: | Size: 736 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 147 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 355 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 364 KiB |
|
After Width: | Height: | Size: 33 MiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 4.6 MiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 90 KiB |