feat: implement doctor API synchronization and add contact page support with media assets
This commit is contained in:
@@ -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);
|
||||
|
||||
+10
-2
@@ -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";
|
||||
}
|
||||
|
||||
+28
-2
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
+11
-6
@@ -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);
|
||||
}
|
||||
|
||||
+133
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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"
|
||||
}
|
||||
}
|
||||
+6
-152
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user