feat: implement doctor data migration and update booking link UI in doctor-detail template

This commit is contained in:
2026-07-29 07:55:55 +07:00
parent 186f70ec32
commit 9bfd3ba5f9
51 changed files with 1070681 additions and 497 deletions
@@ -56,7 +56,7 @@ public class SecurityConfiguration {
"/manage/login", "/css/**", "/images/**", "/js/**", "/vendor/**", "/fonts/**", "/login-assets/**", "/UMass*/**", "/Undergraduate*/**",
"/favicon.ico", "/favicons/**", "/flex-finish/**", "/contact-us/**", "/uploads/**", "/upload/**", "/api/manage/snippets/**", "/page/**", "/news/article/**", "/post/**", "/error",
"/about-us", "/specialty", "/doctor", "/bac-si/**", "/service", "/health-library", "/news-and-events", "/patient-support", "/medical-expert", "/umcers", "/bidding", "/contact-us", "/theme-assets/**", "/dao-tao/**", "/chuyen-khoa", "/chuyen-khoa/**",
"/Đào tạo tại UMC_files/**")
"/Đào tạo tại UMC_files/**", "/dat-lich", "/dat-lich/**", "/api/public/doctor-schedule/**")
.permitAll()
.requestMatchers(HttpMethod.POST, "/api/manage/media/upload").permitAll()
.requestMatchers(HttpMethod.GET, "/swagger-ui/**", "/v3/api-docs/**").permitAll()
@@ -267,9 +267,9 @@ public class PageController {
java.util.List<com.sisvietnamvn.web.domain.Doctor> doctors = doctorService.findAll();
// Show all doctors regardless of active status (so inactive still show up)
// doctors = doctors.stream()
// .filter(d -> Boolean.TRUE.equals(d.getActive()))
// .collect(java.util.stream.Collectors.toList());
doctors = doctors.stream()
.filter(d -> Boolean.TRUE.equals(d.getActive()))
.collect(java.util.stream.Collectors.toList());
// Sort: 00001 and 00002 at the top
doctors.sort((d1, d2) -> {
@@ -411,6 +411,18 @@ public class PageController {
return "page";
}
@GetMapping("/dat-lich")
public String getDatLichPage(Model model) {
model.addAttribute("activeTheme", "umass");
model.addAttribute("cachedDoctors", doctorApiSyncService.getCachedDoctors());
return "pages/dat-lich";
}
@GetMapping("/api/public/doctor-schedule/{doctorCode}")
@org.springframework.web.bind.annotation.ResponseBody
public org.springframework.http.ResponseEntity<List<Map<String, Object>>> getPublicSchedule(@PathVariable String doctorCode) {
return getManualSchedule(doctorCode);
}
@GetMapping("/manage/doctor-scheduler")
public String getQuanLyLichKham(Model model) {
@@ -45,47 +45,6 @@ public class ManageDoctorController {
populateModel(model, new Doctor(), true);
return "manage/doctors/index";
}
@GetMapping("/import-csv")
@ResponseBody
public String importCsv() {
try {
String file = "/home/x79/sisvietnamvn_01/Media/HÌNH ẢNH BÁC SĨ NGỒI PHÒNG KHÁM/ALL_IMAGES/DANH SÁCH BÁC SĨ NGỒI PHÒNG KHÁM.csv";
java.util.List<String> lines = java.nio.file.Files.readAllLines(java.nio.file.Paths.get(file));
int updatedCount = 0;
for (int i = 1; i < lines.size(); i++) {
String line = lines.get(i);
String[] parts = line.split(",", -1);
if (parts.length >= 7) {
String doctorCode = parts[1].trim();
String specialtyName = parts[6].trim();
if (!doctorCode.isEmpty() && !specialtyName.isEmpty()) {
java.util.Optional<Doctor> docOpt = doctorService.findByDoctorCode(doctorCode);
if (docOpt.isPresent()) {
Doctor doctor = docOpt.get();
// Find or create specialty
java.util.List<com.sisvietnamvn.web.domain.Specialty> specs = specialtyService.findAll();
com.sisvietnamvn.web.domain.Specialty spec = specs.stream().filter(s -> s.getName().equalsIgnoreCase(specialtyName)).findFirst().orElse(null);
if (spec == null) {
spec = new com.sisvietnamvn.web.domain.Specialty();
spec.setName(specialtyName);
spec = specialtyService.save(spec);
}
doctor.setSpecialty(spec);
doctorService.save(doctor);
updatedCount++;
}
}
}
}
return "Import success! Updated " + updatedCount + " doctors.";
} catch (Exception e) {
return "Error: " + e.getMessage();
}
}
@PostMapping
public String createDoctor(@Valid @ModelAttribute("doctor") Doctor doctor,
@@ -60,6 +60,33 @@ public class Doctor extends AbstractAuditingEntity<Long> {
@Column(name = "active")
private Boolean active = true;
@Size(max = 255)
@Column(name = "position", length = 255)
private String position;
@Size(max = 20)
@Column(name = "gender", length = 20)
private String gender;
@Size(max = 20)
@Column(name = "birthday", length = 20)
private String birthday;
@Size(max = 50)
@Column(name = "phone_number", length = 50)
private String phoneNumber;
@Size(max = 100)
@Column(name = "email", length = 100)
private String email;
@Size(max = 255)
@Column(name = "address", length = 255)
private String address;
@Column(name = "display_order")
private Integer displayOrder;
// jhipster-needle-entity-add-field - JHipster will add fields here
// --- Getters and Setters ---
@@ -153,6 +180,62 @@ public class Doctor extends AbstractAuditingEntity<Long> {
this.active = active;
}
public String getPosition() {
return position;
}
public void setPosition(String position) {
this.position = position;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String getBirthday() {
return birthday;
}
public void setBirthday(String birthday) {
this.birthday = birthday;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Integer getDisplayOrder() {
return displayOrder;
}
public void setDisplayOrder(Integer displayOrder) {
this.displayOrder = displayOrder;
}
// --- End Getters and Setters ---
// --- equals, hashCode, toString ---
@@ -178,6 +261,9 @@ public class Doctor extends AbstractAuditingEntity<Long> {
", avatarUrl='" + getAvatarUrl() + "'" +
", bookingUrl='" + getBookingUrl() + "'" +
", doctorCode='" + getDoctorCode() + "'" +
", position='" + getPosition() + "'" +
", gender='" + getGender() + "'" +
", email='" + getEmail() + "'" +
"}";
}
}
@@ -1,13 +1,27 @@
package com.sisvietnamvn.web.domain;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.io.Serial;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.Lob;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
/**
* A Post entity representing a blog/news article.
* Maps to the "sis_post" database table.
@@ -227,4 +241,9 @@ public class Post extends AbstractAuditingEntity<Long> {
", status='" + getStatus() + "'" +
"}";
}
@jakarta.persistence.Transient
public String getImageUrl() {
return this.featuredImage;
}
}
@@ -131,44 +131,72 @@ public class SwiperSliderPlugin {
private Optional<SwiperSliderAdminController.SliderGroup> findGroupOrSeed(String slug) {
String json = settingService.getValue(SETTING_KEY, "[]");
List<SwiperSliderAdminController.SliderGroup> groups = parseGroups(json);
final String searchSlug = ("dao-tao-01".equals(slug)) ? "gallery" : slug;
Optional<SwiperSliderAdminController.SliderGroup> groupOpt = groups.stream()
.filter(g -> slug.equals(g.slug()))
.filter(g -> slug.equals(g.slug()) || searchSlug.equals(g.slug()))
.findFirst();
// If UMC_files is present, we replace all data with defaults (only happens once during migration)
if (json.contains("UMC_files")) {
// If UMC_files is present or slug not found, re-seed full data
if (groupOpt.isEmpty() || json.contains("UMC_files")) {
json = forceSeedAllData();
groups = parseGroups(json);
groupOpt = groups.stream()
.filter(g -> slug.equals(g.slug()))
.filter(g -> slug.equals(g.slug()) || searchSlug.equals(g.slug()))
.findFirst();
}
return groupOpt;
}
private String forceSeedAllData() {
String sampleData = "[{\"slug\":\"gallery\",\"name\":\"Gallery Đào Tạo\",\"items\":[" +
private static final String DEFAULT_SLIDER_DATA = "[" +
"{\"slug\":\"dao-tao-01\",\"name\":\"Gallery Đào Tạo\",\"items\":[" +
"{\"title\":\"Khóa đào tạo 1\",\"imageUrl\":\"/images/dao-tao/image-6.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Khóa đào tạo 2\",\"imageUrl\":\"/images/dao-tao/image-5.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Khóa đào tạo 3\",\"imageUrl\":\"/images/dao-tao/image-4.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}" +
"{\"title\":\"Khóa đào tạo 3\",\"imageUrl\":\"/images/dao-tao/image-4.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Khóa đào tạo 4\",\"imageUrl\":\"/images/dao-tao/image-3.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Khóa đào tạo 5\",\"imageUrl\":\"/images/dao-tao/image-2.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Khóa đào tạo 6\",\"imageUrl\":\"/images/dao-tao/image-1.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}" +
"]},{\"slug\":\"gallery\",\"name\":\"Gallery Đào Tạo\",\"items\":[" +
"{\"title\":\"Khóa đào tạo 1\",\"imageUrl\":\"/images/dao-tao/image-6.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Khóa đào tạo 2\",\"imageUrl\":\"/images/dao-tao/image-5.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Khóa đào tạo 3\",\"imageUrl\":\"/images/dao-tao/image-4.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Khóa đào tạo 4\",\"imageUrl\":\"/images/dao-tao/image-3.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Khóa đào tạo 5\",\"imageUrl\":\"/images/dao-tao/image-2.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Khóa đào tạo 6\",\"imageUrl\":\"/images/dao-tao/image-1.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}" +
"]},{\"slug\":\"chung-chi\",\"name\":\"Cấp chứng chỉ\",\"items\":[" +
"{\"title\":\"Lớp chứng chỉ 1\",\"imageUrl\":\"/images/dao-tao/image-3.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Lớp chứng chỉ 2\",\"imageUrl\":\"/images/dao-tao/image-2.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Lớp chứng chỉ 3\",\"imageUrl\":\"/images/dao-tao/image-1.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}" +
"{\"title\":\"Lớp chứng chỉ 3\",\"imageUrl\":\"/images/dao-tao/image-1.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Lớp chứng chỉ 4\",\"imageUrl\":\"/images/dao-tao/training-1.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Lớp chứng chỉ 5\",\"imageUrl\":\"/images/dao-tao/training-2.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Lớp chứng chỉ 6\",\"imageUrl\":\"/images/dao-tao/training-3.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}" +
"]},{\"slug\":\"giay-chung-nhan\",\"name\":\"Cấp giấy chứng nhận\",\"items\":[" +
"{\"title\":\"Giấy chứng nhận 1\",\"imageUrl\":\"/images/dao-tao/training-4.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Giấy chứng nhận 2\",\"imageUrl\":\"/images/dao-tao/training-3.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}" +
"{\"title\":\"Giấy chứng nhận 2\",\"imageUrl\":\"/images/dao-tao/training-3.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Giấy chứng nhận 3\",\"imageUrl\":\"/images/dao-tao/training-2.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Giấy chứng nhận 4\",\"imageUrl\":\"/images/dao-tao/training-1.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Giấy chứng nhận 5\",\"imageUrl\":\"/images/dao-tao/image-5.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Giấy chứng nhận 6\",\"imageUrl\":\"/images/dao-tao/image-6.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}" +
"]},{\"slug\":\"xac-nhan-thuc-hanh\",\"name\":\"Đào tạo cấp giấy xác nhận quá trình thực hành\",\"items\":[" +
"{\"title\":\"Xác nhận thực hành 1\",\"imageUrl\":\"/images/dao-tao/PTNS2707.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Xác nhận thực hành 2\",\"imageUrl\":\"/images/dao-tao/training-2.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}" +
"{\"title\":\"Xác nhận thực hành 2\",\"imageUrl\":\"/images/dao-tao/training-2.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Xác nhận thực hành 3\",\"imageUrl\":\"/images/dao-tao/training-3.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Xác nhận thực hành 4\",\"imageUrl\":\"/images/dao-tao/training-4.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Xác nhận thực hành 5\",\"imageUrl\":\"/images/dao-tao/image-1.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Xác nhận thực hành 6\",\"imageUrl\":\"/images/dao-tao/image-2.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}" +
"]},{\"slug\":\"hinh-anh-thuc-hanh\",\"name\":\"Hình ảnh thực hành lâm sàng\",\"items\":[" +
"{\"title\":\"Thực hành lâm sàng 1\",\"imageUrl\":\"/images/dao-tao/training-1.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Thực hành lâm sàng 2\",\"imageUrl\":\"/images/dao-tao/training-2.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}" +
"{\"title\":\"Thực hành lâm sàng 2\",\"imageUrl\":\"/images/dao-tao/training-2.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Thực hành lâm sàng 3\",\"imageUrl\":\"/images/dao-tao/training-3.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Thực hành lâm sàng 4\",\"imageUrl\":\"/images/dao-tao/training-4.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Thực hành lâm sàng 5\",\"imageUrl\":\"/images/dao-tao/image-4.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}," +
"{\"title\":\"Thực hành lâm sàng 6\",\"imageUrl\":\"/images/dao-tao/image-5.jpeg\",\"linkUrl\":\"\",\"description\":\"\"}" +
"]}]";
settingService.setValue(SETTING_KEY, sampleData);
LOG.info("Forced seeding of default swiper slider data for all 5 training sliders");
return sampleData;
private String forceSeedAllData() {
settingService.setValue(SETTING_KEY, DEFAULT_SLIDER_DATA);
LOG.info("Forced seeding of default swiper slider data for all training sliders");
return DEFAULT_SLIDER_DATA;
}
private List<SwiperSliderAdminController.SliderGroup> parseGroups(String json) {
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.6.xsd">
<changeSet id="20260728163000-1" author="antigravity">
<addColumn tableName="sis_doctor">
<column name="position" type="varchar(255)">
<constraints nullable="true" />
</column>
<column name="gender" type="varchar(20)">
<constraints nullable="true" />
</column>
<column name="birthday" type="varchar(20)">
<constraints nullable="true" />
</column>
<column name="phone_number" type="varchar(50)">
<constraints nullable="true" />
</column>
<column name="email" type="varchar(100)">
<constraints nullable="true" />
</column>
<column name="address" type="varchar(255)">
<constraints nullable="true" />
</column>
<column name="display_order" type="int">
<constraints nullable="true" />
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.6.xsd">
<changeSet id="20260728164000-1" author="antigravity">
<sqlFile path="config/liquibase/data/import_doctors.sql"
relativeToChangelogFile="false"
encoding="utf8"
splitStatements="true"
stripComments="true"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.6.xsd">
<changeSet id="20260728165000-1" author="antigravity">
<sqlFile path="config/liquibase/data/map_specialties.sql"
relativeToChangelogFile="false"
encoding="utf8"
splitStatements="false"
stripComments="true"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.6.xsd">
<changeSet id="20260728166000-1" author="antigravity">
<sqlFile path="config/liquibase/data/update_active_doctors.sql"
relativeToChangelogFile="false"
encoding="utf8"
splitStatements="true"
stripComments="true"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.6.xsd">
<changeSet id="20260728171000-1" author="antigravity">
<sqlFile path="config/liquibase/data/merge_doctors.sql"
relativeToChangelogFile="false"
encoding="utf8"
splitStatements="true"
stripComments="true"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.6.xsd">
<changeSet id="20260728172000-1" author="antigravity">
<sqlFile path="config/liquibase/data/merge_doctors.sql"
relativeToChangelogFile="false"
encoding="utf8"
splitStatements="true"
stripComments="true"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.6.xsd">
<changeSet id="20260728173000-1" author="antigravity">
<sqlFile path="config/liquibase/data/merge_doctors.sql"
relativeToChangelogFile="false"
encoding="utf8"
splitStatements="false"
stripComments="true"/>
</changeSet>
</databaseChangeLog>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,4 @@
BEGIN
COMMIT;
END;
/
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,3 @@
UPDATE sis_doctor SET active = 0 WHERE doctor_code IS NULL OR doctor_code = '';
UPDATE sis_doctor SET active = 1 WHERE doctor_code IS NOT NULL AND doctor_code != '';
COMMIT;
@@ -52,4 +52,9 @@
<include file="config/liquibase/changelog/20260724151500_alter_setting_value_to_clob.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260728140500_add_contact_fields_to_page.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260728160000_add_specialty_category.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260728163000_add_xml_fields_to_doctor.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260728164000_import_doctors_data.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260728165000_map_specialties.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260728166000_update_active_doctors.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260728173000_fix_xml_specialties.xml" relativeToChangelogFile="false"/>
</databaseChangeLog>
@@ -81,31 +81,23 @@
<div style="-webkit-text-stroke-width:0px;border-style:solid;border-width:0px;box-sizing:border-box;color:rgb(51, 51, 51);font-family:Roboto, ui-sans-serif, system-ui, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;;font-size:16px;font-style:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-weight:400;letter-spacing:normal;margin:0px;orphans:2;padding:0px;text-align:justify;text-decoration-color:initial;text-decoration-style:initial;text-decoration-thickness:initial;text-indent:0px;text-transform:none;white-space:normal;widows:2;word-spacing:0px;">Phòng KHĐT cùng Đơn vị lập kế hoạch, dự trù kinh phí cho các lớp đào tạo với các hình thức như đào tạo y khoa liên tục (CME), hội nghị, hội thảo, lớp học, … Những chương trình này có thể tổ chức tại Bệnh viện hoặc tổ chức tại các địa điểm bên ngoài tùy theo nhu cầu thực tế để đưa hoạt động đào tạo thực sự trở thành một mũi nhọn, một đặc trưng của bệnh viện.</div>
</div>
<div>
<div class="relative md:flex md:items-center md:gap-x-2">
<button class="btn-navigation md:size-[42px] size-[32px] flex-shrink-0 items-center justify-center rounded-full btn-navigation-gallery-prev md:flex hidden">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-up size-4 -rotate-90">
<path d="m18 15-6-6-6 6"></path>
</svg>
</button>
<div class="swiper swiper-gallery w-full">
<div class="swiper-wrapper">
<th:block th:utext="${hookManager.applyFilters('swiper_slider_items', '', 'gallery')}"></th:block>
</div>
<div class="relative md:flex md:items-center md:gap-x-2">
<button class="btn-navigation md:size-[42px] size-[32px] flex-shrink-0 items-center justify-center rounded-full btn-navigation-dao-tao-01-prev md:flex hidden">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-up size-4 -rotate-90">
<path d="m18 15-6-6-6 6"></path>
</svg>
</button>
<div class="swiper swiper-dao-tao-01 w-full">
<div class="swiper-wrapper">
<th:block th:utext="${hookManager.applyFilters('swiper_slider_items', '', 'dao-tao-01')}"></th:block>
</div>
</div>
<button class="btn-navigation md:size-[42px] size-[32px] flex-shrink-0 items-center justify-center rounded-full btn-navigation-gallery-next md:flex hidden">
<button class="btn-navigation md:size-[42px] size-[32px] flex-shrink-0 items-center justify-center rounded-full btn-navigation-dao-tao-01-next md:flex hidden">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down size-4 -rotate-90">
<path d="m6 9 6 6 6-6"></path>
</svg>
</button>
<div class="swiper-pagination swiper-pagination-training-gallery space-x-[13px] !relative [&amp;_.swiper-pagination-bullet-active]:!bg-primary-600 [&amp;_.swiper-pagination-bullet-active]:!opacity-100 [&amp;_.swiper-pagination-bullet-active]:before:opacity-100 mt-6 flex justify-center [&amp;_.swiper-pagination-bullet]:!relative [&amp;_.swiper-pagination-bullet]:before:content-[&#39;&#39;] [&amp;_.swiper-pagination-bullet]:before:size-[14px] [&amp;_.swiper-pagination-bullet]:before:bg-transparent [&amp;_.swiper-pagination-bullet]:before:rounded-full [&amp;_.swiper-pagination-bullet]:before:border [&amp;_.swiper-pagination-bullet]:before:border-primary-600 md:[&amp;_.swiper-pagination-bullet]:before:top-[-3px] [&amp;_.swiper-pagination-bullet]:before:top-[-3px] [&amp;_.swiper-pagination-bullet]:before:left-[-2.7px] [&amp;_.swiper-pagination-bullet]:before:absolute [&amp;_.swiper-pagination-bullet]:before:block [&amp;_.swiper-pagination-bullet]:before:opacity-0 [&amp;_.swiper-pagination-bullet]:!size-[8px] [&amp;_.swiper-pagination-bullet]:!bg-gray-600 md:hidden swiper-pagination-clickable swiper-pagination-bullets swiper-pagination-horizontal">
<span class="swiper-pagination-bullet swiper-pagination-bullet-active"></span>
<span class="swiper-pagination-bullet"></span>
<span class="swiper-pagination-bullet"></span>
<span class="swiper-pagination-bullet"></span>
<span class="swiper-pagination-bullet"></span>
<span class="swiper-pagination-bullet"></span>
</div>
</div>
</div>
</div>
</section>
<section class="xl:py-12 md:py-8 py-6 bg-[#f6f6f6] xl:space-y-12 md:space-y-8 space-y-6">
@@ -116,12 +108,12 @@
</div>
<div class="flex items-center gap-4">
<div class="flex md:gap-2 gap-1">
<button class="btn-navigation-khoa-hoc-prev md:size-10 size-8 flex items-center justify-center rounded-full bg-white shadow-sm border border-gray-100 hover:bg-primary-600 hover:text-white transition-colors cursor-pointer group swiper-button-disabled swiper-button-lock" disabled="">
<button class="btn-navigation-khoa-hoc-prev md:size-10 size-8 flex items-center justify-center rounded-full bg-white shadow-sm border border-gray-100 hover:bg-primary-600 hover:text-white transition-colors cursor-pointer group">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-up size-4 -rotate-90">
<path d="m18 15-6-6-6 6"></path>
</svg>
</button>
<button class="btn-navigation-khoa-hoc-next md:size-10 size-8 flex items-center justify-center rounded-full bg-white shadow-sm border border-gray-100 hover:bg-primary-600 hover:text-white transition-colors cursor-pointer group swiper-button-disabled swiper-button-lock" disabled="">
<button class="btn-navigation-khoa-hoc-next md:size-10 size-8 flex items-center justify-center rounded-full bg-white shadow-sm border border-gray-100 hover:bg-primary-600 hover:text-white transition-colors cursor-pointer group">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down size-4 -rotate-90">
<path d="m6 9 6 6 6-6"></path>
</svg>
@@ -131,22 +123,39 @@
</div>
<div class="relative max-md:px-4 md:mb-6 mb-4 lg:mb-8">
<div class="swiper swiper-khoa-hoc w-full">
<div class="swiper-wrapper">
<!-- 1. ĐÃ XÓA gap-3 -->
<div class="swiper-wrapper" style="height: 450px;">
<th:block th:each="post : ${daoTaoPosts}">
<div class="swiper-slide h-auto max-w-[288px] mr-2 lg:!mr-3 xl:!mr-4" style="width: 288px; max-width: 100%;">
<a th:href="@{/dao-tao/{slug}(slug=${post.slug})}" class="group h-full flex flex-col items-start gap-y-4 lg:gap-y-5 rounded-2xl bg-white p-4 pb-5 shadow-sm transition-shadow hover:shadow-md border border-transparent hover:border-primary-100">
<div class="relative w-full aspect-square overflow-hidden rounded-xl">
<img th:src="${post.imageUrl != null and !#strings.isEmpty(post.imageUrl) ? post.imageUrl : '/images/dao-tao/training-1.jpeg'}" th:alt="${post.title}" class="absolute inset-0 size-full object-cover transition-transform duration-500 group-hover:scale-105">
<!-- 2. Dùng h-auto để Swiper tự động stretch các card cao bằng nhau -->
<div class="swiper-slide h-auto" style="width: 288px; max-width: 100%; margin: 5px; background-color: var(--color-white);">
<!-- Thẻ a (card) đã set h-full để giãn kịch trần -->
<a th:href="@{/dao-tao/{slug}(slug=${post.slug})}" class="group h-full flex flex-col items-start">
<!-- 3. Đã thêm overflow-hidden vào khung ảnh -->
<div class="w-full rounded-xl mb-3">
<img th:src="${post.imageUrl != null and !#strings.isEmpty(post.imageUrl) ? post.imageUrl : '/images/dao-tao/training-1.jpeg'}"
th:alt="${post.title}"
class="size-full aspect-square object-cover transition-transform duration-500 group-hover:scale-105">
</div>
<div class="flex flex-1 flex-col justify-between w-full">
<h3 class="heading-5 text-gray-900 group-hover:text-primary-600 transition-colors line-clamp-3" th:text="${post.title}">Tiêu đề khóa học</h3>
<!-- 4. Gom Tiêu đề và Trích dẫn vào chung 1 thẻ flex-1 -->
<div class="flex flex-1 flex-col justify-start w-full gap-y-2">
<h3 class="heading-5 text-gray-900 group-hover:text-primary-600 transition-colors line-clamp-3"
th:text="${post.title}">Tiêu đề khóa học</h3>
<!-- Đoạn text trích dẫn -->
<p class="body-3 text-gray-600 line-clamp-3"
th:text="${post.excerpt}">Giới thiệu ngắn gọn</p>
</div>
</a>
</div>
</th:block>
</div>
</div>
</div>
</div>
<div class="lg:container max-lg:pl-5 max-md:px-0">
<div class="flex justify-between items-end md:mb-6 mb-4 lg:mb-8 max-lg:pr-5 max-md:px-4">
@@ -155,12 +164,12 @@
</div>
<div class="flex items-center gap-4">
<div class="flex md:gap-2 gap-1">
<button class="btn-navigation-cap-chung-chi-prev md:size-10 size-8 flex items-center justify-center rounded-full bg-white shadow-sm border border-gray-100 hover:bg-primary-600 hover:text-white transition-colors cursor-pointer group swiper-button-disabled swiper-button-lock" disabled="">
<button class="btn-navigation-cap-chung-chi-prev md:size-10 size-8 flex items-center justify-center rounded-full bg-white shadow-sm border border-gray-100 hover:bg-primary-600 hover:text-white transition-colors cursor-pointer group">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-up size-4 -rotate-90">
<path d="m18 15-6-6-6 6"></path>
</svg>
</button>
<button class="btn-navigation-cap-chung-chi-next md:size-10 size-8 flex items-center justify-center rounded-full bg-white shadow-sm border border-gray-100 hover:bg-primary-600 hover:text-white transition-colors cursor-pointer group swiper-button-disabled swiper-button-lock" disabled="">
<button class="btn-navigation-cap-chung-chi-next md:size-10 size-8 flex items-center justify-center rounded-full bg-white shadow-sm border border-gray-100 hover:bg-primary-600 hover:text-white transition-colors cursor-pointer group">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down size-4 -rotate-90">
<path d="m6 9 6 6 6-6"></path>
</svg>
@@ -183,12 +192,12 @@
</div>
<div class="flex items-center gap-4">
<div class="flex md:gap-2 gap-1">
<button class="btn-navigation-cap-giay-chung-nhan-prev md:size-10 size-8 flex items-center justify-center rounded-full bg-white shadow-sm border border-gray-100 hover:bg-primary-600 hover:text-white transition-colors cursor-pointer group swiper-button-disabled swiper-button-lock" disabled="">
<button class="btn-navigation-cap-giay-chung-nhan-prev md:size-10 size-8 flex items-center justify-center rounded-full bg-white shadow-sm border border-gray-100 hover:bg-primary-600 hover:text-white transition-colors cursor-pointer group">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-up size-4 -rotate-90">
<path d="m18 15-6-6-6 6"></path>
</svg>
</button>
<button class="btn-navigation-cap-giay-chung-nhan-next md:size-10 size-8 flex items-center justify-center rounded-full bg-white shadow-sm border border-gray-100 hover:bg-primary-600 hover:text-white transition-colors cursor-pointer group swiper-button-disabled swiper-button-lock" disabled="">
<button class="btn-navigation-cap-giay-chung-nhan-next md:size-10 size-8 flex items-center justify-center rounded-full bg-white shadow-sm border border-gray-100 hover:bg-primary-600 hover:text-white transition-colors cursor-pointer group">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down size-4 -rotate-90">
<path d="m6 9 6 6 6-6"></path>
</svg>
@@ -211,12 +220,12 @@
</div>
<div class="flex items-center gap-4">
<div class="flex md:gap-2 gap-1">
<button class="btn-navigation-dao-tao-cap-giay-xac-nhan-qua-trinh-thuc-hanh-prev md:size-10 size-8 flex items-center justify-center rounded-full bg-white shadow-sm border border-gray-100 hover:bg-primary-600 hover:text-white transition-colors cursor-pointer group swiper-button-disabled swiper-button-lock" disabled="">
<button class="btn-navigation-dao-tao-cap-giay-xac-nhan-qua-trinh-thuc-hanh-prev md:size-10 size-8 flex items-center justify-center rounded-full bg-white shadow-sm border border-gray-100 hover:bg-primary-600 hover:text-white transition-colors cursor-pointer group">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-up size-4 -rotate-90">
<path d="m18 15-6-6-6 6"></path>
</svg>
</button>
<button class="btn-navigation-dao-tao-cap-giay-xac-nhan-qua-trinh-thuc-hanh-next md:size-10 size-8 flex items-center justify-center rounded-full bg-white shadow-sm border border-gray-100 hover:bg-primary-600 hover:text-white transition-colors cursor-pointer group swiper-button-disabled swiper-button-lock" disabled="">
<button class="btn-navigation-dao-tao-cap-giay-xac-nhan-qua-trinh-thuc-hanh-next md:size-10 size-8 flex items-center justify-center rounded-full bg-white shadow-sm border border-gray-100 hover:bg-primary-600 hover:text-white transition-colors cursor-pointer group">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down size-4 -rotate-90">
<path d="m6 9 6 6 6-6"></path>
</svg>
@@ -301,10 +310,6 @@
</div>
</div>
</section>
<!--$-->
<!--/$-->
<!--$-->
<!--/$-->
</main>
<div class="fixed bottom-6 right-6 z-[49] flex flex-col gap-3 items-end">
<a href="tel:19002827" aria-label="Gọi điện thoại" class="flex h-10 w-10 items-center justify-center rounded-full bg-primary-700 shadow-lg transition-colors hover:bg-primary-800" style="opacity: 1; transform: none; transform-origin: 50% 50% 0px;">
@@ -318,358 +323,7 @@
</svg>
</button>
</div>
<!--/$-->
<next-route-announcer style="position: absolute;">
<template shadowrootmode="open">
<div aria-live="assertive" id="__next-route-announcer__" role="alert" style="position: absolute; border: 0px; height: 1px; margin: -1px; padding: 0px; width: 1px; clip: rect(0px, 0px, 0px, 0px); overflow: hidden; white-space: nowrap; overflow-wrap: normal;"></div>
</template>
</next-route-announcer>
<div id="easychatgpt-widget" data-ecg-initialized="true" style="--ui-logo: https://assets.geteasy.ai/workspaces/2353/bots/2770/public/0fd2dbe4-63bf-4d8e-871f-ab903f800fca.png; --ui-name: Trợ lý AI của UMC; --ui-size: medium; --ui-theme: light; --ui-alignment-offset-mobile-vertical: 125px; --ui-alignment-offset-mobile-horizontal: 15px; --ui-alignment-offset-desktop-vertical: 130px; --ui-alignment-offset-desktop-horizontal: 20px; --ui-alignment-position: right; --ui-icon-image: https://assets.geteasy.ai/workspaces/2353/bots/2770/public/5026d034-feea-46db-a0c1-60c078ef5d5d.png; --ui-bubble_chat-offset-mobile-size: small; --ui-bubble_chat-offset-mobile-custom: 60px; --ui-bubble_chat-offset-desktop-size: small; --ui-bubble_chat-offset-desktop-custom: 60px; --ui-primary-color: rgb(8, 82, 158); --ui-chat-button-icon: &lt;svg width=&quot;30&quot; height=&quot;40&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; class=&quot;block z-10 text-white align-middle border-gray-200&quot;&gt;&lt;path d=&quot;m3 21 1.9-5.7a8.5 8.5 0 1 1 3.8 3.8z&quot; class=&quot;border-gray-200&quot;&gt;&lt;/path&gt;&lt;/svg&gt;; --ui-automatically-expand: false; --ui-show-assistant-avatar: true;">
<template shadowrootmode="open">
<div data-v-app="">
<div workspaceuuid="d734fb80-4d39-4fd3-8b8c-d6af95ad4489">
<div class="chat-widget position-right btn-size-desktop-small btn-size-mobile-small" style="display: none; --size-desktop-custom: 60px; --size-mobile-custom: 60px; --z-index-chatview: 9999999999; --z-index-bubblechat: 999999990;">
<div class="w-screen grow sm:w-[425px] sm:h-[720px] sm:max-h-[calc(100vh-6rem)] z-[var(--z-index-chatview)] overflow-hidden ecg-chat-view sm:rounded-3xl box-shadow-md max-sm:border-t-0 max-sm:border-x-0 fixed max-sm:right-0 sm:right-4 duration-500 opacity-0 invisible translate-y-[150%] h-dvh rounded-none max-sm:top-0">
<div id="ei-chat-container" class="flex flex-col h-full w-full bg-gray-50 h-full w-full">
<div class="flex-1 overflow-hidden h-full">
<!---->
<div id="chat-view" class="flex flex-col overflow-hidden bg-white h-full w-full pb-1">
<div class="relative ecg-widget-header">
<div class="flex items-center justify-between p-3.5 text-white bg-primary">
<div class="flex items-center gap-2">
<button class="p-1.5 rounded-lg hover:bg-white/10 text-white" aria-label="Quay lại">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" size="20" class="lucide lucide-chevron-left-icon">
<path d="m15 18-6-6 6-6"></path>
</svg>
</button>
<!---->
<h2 class="text-base font-medium tracking-tight">Trợ lý AI của UMC</h2>
</div>
<div class="flex gap-1.5">
<!---->
<div class="flex items-center justify-center rounded-lg size-8 bg-white/10 hover:bg-white/20 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" size="14" class="lucide lucide-refresh-ccw-icon">
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path>
<path d="M3 3v5h5"></path>
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path>
<path d="M16 16h5v5"></path>
</svg>
</div>
<button aria-label="eac-close-chat" class="size-8 rounded-lg flex items-center justify-center bg-white/10 hover:bg-white/20">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" size="16" class="lucide lucide-minus-icon">
<path d="M5 12h14"></path>
</svg>
</button>
</div>
</div>
</div>
<div class="messages-wrapper">
<div class="chat-banners block chat-content-item">
<div class="embla-wrapper">
<div class="embla">
<div class="embla__container" style="transform: translate3d(0px, 0px, 0px);">
<div class="embla__slide w-full h-[150px]">
<div target="_blank" class="block w-full h-full">
<img src="https://assets.geteasy.ai/workspaces/2353/bots/2770/public/fabf8d12-7b48-4257-a754-78288d866339.png" alt="ecg-banner" class="object-cover w-full h-full">
</div>
</div>
</div>
</div>
<!---->
</div>
<button class="close-banner-btn">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-3">
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</button>
</div>
<div class="flex flex-col flex-grow px-2 pt-4 space-y-4 duration-100 lg:px-4 chat-content-item">
<div class="space-y-3 mb-4">
<div class="messages-group message-assistant">
<span class="relative flex flex-shrink-0 overflow-hidden bg-white size-8 message-avatar opacity-0 rounded">
<img src="https://assets.geteasy.ai/workspaces/2353/bots/2770/public/0fd2dbe4-63bf-4d8e-871f-ab903f800fca.png" alt="Trợ lý AI của UMC" class="object-cover w-full h-full">
</span>
<div class="messages">
<div class="relative flex-1 group w-fit max-w-[92%]">
<!---->
<div class="relative w-full mr-auto">
<div class="message message-assistant">
<div class="relative max-w-full w-fit">
<div class="relative px-4 py-3.5 min-w-11 bg-gray-100 rounded-xl prose-sm prose max-sm:select-none">
<p>Xin chào, Em là trợ lý ảo của UMC 👋</p>
</div>
<button type="button" aria-label="Trả lời" title="Trả lời" class="reply-btn opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity duration-200 flex items-center justify-center size-6 rounded-full bg-gray-50 shadow-sm text-gray-500 hover:bg-gray-100 hover:text-gray-600 absolute top-1/2 -translate-y-1/2 -right-7">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-quote-icon size-3">
<path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"></path>
<path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"></path>
</svg>
</button>
<div class="absolute right-1.5 -bottom-3 opacity-0 md:group-hover:opacity-100 transition-opacity duration-300">
<div data-v-76058769="" class="relative reaction-container">
<div data-v-76058769="" class="flex items-center group-reactions">
<!---->
<button data-v-76058769="" class="reaction-button flex items-center justify-center size-[26px] border border-gray-100 shadow-sm rounded-full bg-white select-none transition-[transform,filter,background-color] duration-[150ms,200ms,200ms] ease-[cubic-bezier(0.175,0.885,0.32,1.275),ease,ease] hover:scale-110 active:scale-95 will-change-transform-opacity grayscale text-sm">
<svg data-v-76058769="" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" size="14" class="lucide lucide-heart-icon text-gray-500">
<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"></path>
</svg>
</button>
</div>
<div data-v-76058769="" class="absolute -top-2 z-10 -translate-y-full emoji-picker sm:top-0 origin-center-bottom will-change-[transform,opacity] translate-x-0 -right-0 sm:-right-3">
<div data-v-76058769="" class="flex justify-center items-center px-2 py-0.5 rounded-3xl select-none bg-white/90" style="box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px;">
<button data-v-76058769="" class="emoji-btn bg-transparent border-none cursor-pointer flex items-center justify-center size-[28px] sm:size-[26px] rounded-3xl hover:bg-gray-100 hover:scale-115 relative transition-[transform,background-color] duration-150 ease-in-out will-change-transform" style="--delay: 0s;">
<span data-v-76058769="" class="leading-[100%] text-base sm:text-sm translate-y-[1px]">❤️</span>
<div data-v-76058769="" class="custom-tooltip absolute -top-[25px] left-1/2 -translate-x-1/2 scale-[0.8] bg-black/75 text-white text-[10px] px-1.5 py-0.5 rounded whitespace-nowrap opacity-0 pointer-events-none transition-[opacity,transform] duration-200 ease-in-out z-[60] hidden hover-hover:group-hover/emoji:opacity-100 hover-hover:group-hover/emoji:scale-100 after:content-[&#39;&#39;] after:absolute after:top-full after:left-1/2 after:-ml-1 after:border-4 after:border-solid after:border-t-black/75 after:border-x-transparent after:border-b-transparent">Yêu thích</div>
</button>
<button data-v-76058769="" class="emoji-btn bg-transparent border-none cursor-pointer flex items-center justify-center size-[28px] sm:size-[26px] rounded-3xl hover:bg-gray-100 hover:scale-115 relative transition-[transform,background-color] duration-150 ease-in-out will-change-transform" style="--delay: 0.03s;">
<span data-v-76058769="" class="leading-[100%] text-base sm:text-sm translate-y-[1px]">👍</span>
<div data-v-76058769="" class="custom-tooltip absolute -top-[25px] left-1/2 -translate-x-1/2 scale-[0.8] bg-black/75 text-white text-[10px] px-1.5 py-0.5 rounded whitespace-nowrap opacity-0 pointer-events-none transition-[opacity,transform] duration-200 ease-in-out z-[60] hidden hover-hover:group-hover/emoji:opacity-100 hover-hover:group-hover/emoji:scale-100 after:content-[&#39;&#39;] after:absolute after:top-full after:left-1/2 after:-ml-1 after:border-4 after:border-solid after:border-t-black/75 after:border-x-transparent after:border-b-transparent">Thích</div>
</button>
<button data-v-76058769="" class="emoji-btn bg-transparent border-none cursor-pointer flex items-center justify-center size-[28px] sm:size-[26px] rounded-3xl hover:bg-gray-100 hover:scale-115 relative transition-[transform,background-color] duration-150 ease-in-out will-change-transform" style="--delay: 0.06s;">
<span data-v-76058769="" class="leading-[100%] text-lg sm:text-base">🤣</span>
<div data-v-76058769="" class="custom-tooltip absolute -top-[25px] left-1/2 -translate-x-1/2 scale-[0.8] bg-black/75 text-white text-[10px] px-1.5 py-0.5 rounded whitespace-nowrap opacity-0 pointer-events-none transition-[opacity,transform] duration-200 ease-in-out z-[60] hidden hover-hover:group-hover/emoji:opacity-100 hover-hover:group-hover/emoji:scale-100 after:content-[&#39;&#39;] after:absolute after:top-full after:left-1/2 after:-ml-1 after:border-4 after:border-solid after:border-t-black/75 after:border-x-transparent after:border-b-transparent">Haha</div>
</button>
<button data-v-76058769="" class="emoji-btn bg-transparent border-none cursor-pointer flex items-center justify-center size-[28px] sm:size-[26px] rounded-3xl hover:bg-gray-100 hover:scale-115 relative transition-[transform,background-color] duration-150 ease-in-out will-change-transform" style="--delay: 0.09s;">
<span data-v-76058769="" class="leading-[100%] text-lg sm:text-base">😮</span>
<div data-v-76058769="" class="custom-tooltip absolute -top-[25px] left-1/2 -translate-x-1/2 scale-[0.8] bg-black/75 text-white text-[10px] px-1.5 py-0.5 rounded whitespace-nowrap opacity-0 pointer-events-none transition-[opacity,transform] duration-200 ease-in-out z-[60] hidden hover-hover:group-hover/emoji:opacity-100 hover-hover:group-hover/emoji:scale-100 after:content-[&#39;&#39;] after:absolute after:top-full after:left-1/2 after:-ml-1 after:border-4 after:border-solid after:border-t-black/75 after:border-x-transparent after:border-b-transparent">Wow</div>
</button>
<button data-v-76058769="" class="emoji-btn bg-transparent border-none cursor-pointer flex items-center justify-center size-[28px] sm:size-[26px] rounded-3xl hover:bg-gray-100 hover:scale-115 relative transition-[transform,background-color] duration-150 ease-in-out will-change-transform" style="--delay: 0.12s;">
<span data-v-76058769="" class="leading-[100%] text-lg sm:text-base">😭</span>
<div data-v-76058769="" class="custom-tooltip absolute -top-[25px] left-1/2 -translate-x-1/2 scale-[0.8] bg-black/75 text-white text-[10px] px-1.5 py-0.5 rounded whitespace-nowrap opacity-0 pointer-events-none transition-[opacity,transform] duration-200 ease-in-out z-[60] hidden hover-hover:group-hover/emoji:opacity-100 hover-hover:group-hover/emoji:scale-100 after:content-[&#39;&#39;] after:absolute after:top-full after:left-1/2 after:-ml-1 after:border-4 after:border-solid after:border-t-black/75 after:border-x-transparent after:border-b-transparent">Buồn</div>
</button>
<button data-v-76058769="" class="emoji-btn bg-transparent border-none cursor-pointer flex items-center justify-center size-[28px] sm:size-[26px] rounded-3xl hover:bg-gray-100 hover:scale-115 relative transition-[transform,background-color] duration-150 ease-in-out will-change-transform" style="--delay: 0.15s;">
<span data-v-76058769="" class="leading-[100%] text-base sm:text-sm translate-y-[1px]">👎</span>
<div data-v-76058769="" class="custom-tooltip absolute -top-[25px] left-1/2 -translate-x-1/2 scale-[0.8] bg-black/75 text-white text-[10px] px-1.5 py-0.5 rounded whitespace-nowrap opacity-0 pointer-events-none transition-[opacity,transform] duration-200 ease-in-out z-[60] hidden hover-hover:group-hover/emoji:opacity-100 hover-hover:group-hover/emoji:scale-100 after:content-[&#39;&#39;] after:absolute after:top-full after:left-1/2 after:-ml-1 after:border-4 after:border-solid after:border-t-black/75 after:border-x-transparent after:border-b-transparent">Không thích</div>
</button>
<!---->
</div>
</div>
<!---->
</div>
</div>
</div>
<!---->
</div>
</div>
<!---->
</div>
</div>
</div>
<div id="message-1" class="messages-group message-assistant !mt-2">
<span class="relative flex flex-shrink-0 overflow-hidden bg-white size-8 message-avatar opacity-100 rounded">
<img src="https://assets.geteasy.ai/workspaces/2353/bots/2770/public/0fd2dbe4-63bf-4d8e-871f-ab903f800fca.png" alt="Trợ lý AI của UMC" class="object-cover w-full h-full">
</span>
<div class="messages">
<div class="relative flex-1 group w-fit max-w-[92%]">
<!---->
<div class="relative w-full mr-auto">
<div class="message message-assistant">
<div class="relative max-w-full w-fit">
<div class="relative px-4 py-3.5 min-w-11 bg-gray-100 rounded-xl prose-sm prose max-sm:select-none">
<p>Em rất sẵn lòng hỗ trợ Anh/Chị 😊</p>
</div>
<button type="button" aria-label="Trả lời" title="Trả lời" class="reply-btn opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity duration-200 flex items-center justify-center size-6 rounded-full bg-gray-50 shadow-sm text-gray-500 hover:bg-gray-100 hover:text-gray-600 absolute top-1/2 -translate-y-1/2 -right-7">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-quote-icon size-3">
<path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"></path>
<path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"></path>
</svg>
</button>
<div class="absolute right-1.5 -bottom-3 opacity-100">
<div data-v-76058769="" class="relative reaction-container">
<div data-v-76058769="" class="flex items-center group-reactions">
<!---->
<button data-v-76058769="" class="reaction-button flex items-center justify-center size-[26px] border border-gray-100 shadow-sm rounded-full bg-white select-none transition-[transform,filter,background-color] duration-[150ms,200ms,200ms] ease-[cubic-bezier(0.175,0.885,0.32,1.275),ease,ease] hover:scale-110 active:scale-95 will-change-transform-opacity grayscale text-sm">
<svg data-v-76058769="" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" size="14" class="lucide lucide-heart-icon text-gray-500">
<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"></path>
</svg>
</button>
</div>
<div data-v-76058769="" class="absolute -top-2 z-10 -translate-y-full emoji-picker sm:top-0 origin-center-bottom will-change-[transform,opacity] translate-x-[46%] right-0">
<div data-v-76058769="" class="flex justify-center items-center px-2 py-0.5 rounded-3xl select-none bg-white/90" style="box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px;">
<button data-v-76058769="" class="emoji-btn bg-transparent border-none cursor-pointer flex items-center justify-center size-[28px] sm:size-[26px] rounded-3xl hover:bg-gray-100 hover:scale-115 relative transition-[transform,background-color] duration-150 ease-in-out will-change-transform" style="--delay: 0s;">
<span data-v-76058769="" class="leading-[100%] text-base sm:text-sm translate-y-[1px]">❤️</span>
<div data-v-76058769="" class="custom-tooltip absolute -top-[25px] left-1/2 -translate-x-1/2 scale-[0.8] bg-black/75 text-white text-[10px] px-1.5 py-0.5 rounded whitespace-nowrap opacity-0 pointer-events-none transition-[opacity,transform] duration-200 ease-in-out z-[60] hidden hover-hover:group-hover/emoji:opacity-100 hover-hover:group-hover/emoji:scale-100 after:content-[&#39;&#39;] after:absolute after:top-full after:left-1/2 after:-ml-1 after:border-4 after:border-solid after:border-t-black/75 after:border-x-transparent after:border-b-transparent">Yêu thích</div>
</button>
<button data-v-76058769="" class="emoji-btn bg-transparent border-none cursor-pointer flex items-center justify-center size-[28px] sm:size-[26px] rounded-3xl hover:bg-gray-100 hover:scale-115 relative transition-[transform,background-color] duration-150 ease-in-out will-change-transform" style="--delay: 0.03s;">
<span data-v-76058769="" class="leading-[100%] text-base sm:text-sm translate-y-[1px]">👍</span>
<div data-v-76058769="" class="custom-tooltip absolute -top-[25px] left-1/2 -translate-x-1/2 scale-[0.8] bg-black/75 text-white text-[10px] px-1.5 py-0.5 rounded whitespace-nowrap opacity-0 pointer-events-none transition-[opacity,transform] duration-200 ease-in-out z-[60] hidden hover-hover:group-hover/emoji:opacity-100 hover-hover:group-hover/emoji:scale-100 after:content-[&#39;&#39;] after:absolute after:top-full after:left-1/2 after:-ml-1 after:border-4 after:border-solid after:border-t-black/75 after:border-x-transparent after:border-b-transparent">Thích</div>
</button>
<button data-v-76058769="" class="emoji-btn bg-transparent border-none cursor-pointer flex items-center justify-center size-[28px] sm:size-[26px] rounded-3xl hover:bg-gray-100 hover:scale-115 relative transition-[transform,background-color] duration-150 ease-in-out will-change-transform" style="--delay: 0.06s;">
<span data-v-76058769="" class="leading-[100%] text-lg sm:text-base">🤣</span>
<div data-v-76058769="" class="custom-tooltip absolute -top-[25px] left-1/2 -translate-x-1/2 scale-[0.8] bg-black/75 text-white text-[10px] px-1.5 py-0.5 rounded whitespace-nowrap opacity-0 pointer-events-none transition-[opacity,transform] duration-200 ease-in-out z-[60] hidden hover-hover:group-hover/emoji:opacity-100 hover-hover:group-hover/emoji:scale-100 after:content-[&#39;&#39;] after:absolute after:top-full after:left-1/2 after:-ml-1 after:border-4 after:border-solid after:border-t-black/75 after:border-x-transparent after:border-b-transparent">Haha</div>
</button>
<button data-v-76058769="" class="emoji-btn bg-transparent border-none cursor-pointer flex items-center justify-center size-[28px] sm:size-[26px] rounded-3xl hover:bg-gray-100 hover:scale-115 relative transition-[transform,background-color] duration-150 ease-in-out will-change-transform" style="--delay: 0.09s;">
<span data-v-76058769="" class="leading-[100%] text-lg sm:text-base">😮</span>
<div data-v-76058769="" class="custom-tooltip absolute -top-[25px] left-1/2 -translate-x-1/2 scale-[0.8] bg-black/75 text-white text-[10px] px-1.5 py-0.5 rounded whitespace-nowrap opacity-0 pointer-events-none transition-[opacity,transform] duration-200 ease-in-out z-[60] hidden hover-hover:group-hover/emoji:opacity-100 hover-hover:group-hover/emoji:scale-100 after:content-[&#39;&#39;] after:absolute after:top-full after:left-1/2 after:-ml-1 after:border-4 after:border-solid after:border-t-black/75 after:border-x-transparent after:border-b-transparent">Wow</div>
</button>
<button data-v-76058769="" class="emoji-btn bg-transparent border-none cursor-pointer flex items-center justify-center size-[28px] sm:size-[26px] rounded-3xl hover:bg-gray-100 hover:scale-115 relative transition-[transform,background-color] duration-150 ease-in-out will-change-transform" style="--delay: 0.12s;">
<span data-v-76058769="" class="leading-[100%] text-lg sm:text-base">😭</span>
<div data-v-76058769="" class="custom-tooltip absolute -top-[25px] left-1/2 -translate-x-1/2 scale-[0.8] bg-black/75 text-white text-[10px] px-1.5 py-0.5 rounded whitespace-nowrap opacity-0 pointer-events-none transition-[opacity,transform] duration-200 ease-in-out z-[60] hidden hover-hover:group-hover/emoji:opacity-100 hover-hover:group-hover/emoji:scale-100 after:content-[&#39;&#39;] after:absolute after:top-full after:left-1/2 after:-ml-1 after:border-4 after:border-solid after:border-t-black/75 after:border-x-transparent after:border-b-transparent">Buồn</div>
</button>
<button data-v-76058769="" class="emoji-btn bg-transparent border-none cursor-pointer flex items-center justify-center size-[28px] sm:size-[26px] rounded-3xl hover:bg-gray-100 hover:scale-115 relative transition-[transform,background-color] duration-150 ease-in-out will-change-transform" style="--delay: 0.15s;">
<span data-v-76058769="" class="leading-[100%] text-base sm:text-sm translate-y-[1px]">👎</span>
<div data-v-76058769="" class="custom-tooltip absolute -top-[25px] left-1/2 -translate-x-1/2 scale-[0.8] bg-black/75 text-white text-[10px] px-1.5 py-0.5 rounded whitespace-nowrap opacity-0 pointer-events-none transition-[opacity,transform] duration-200 ease-in-out z-[60] hidden hover-hover:group-hover/emoji:opacity-100 hover-hover:group-hover/emoji:scale-100 after:content-[&#39;&#39;] after:absolute after:top-full after:left-1/2 after:-ml-1 after:border-4 after:border-solid after:border-t-black/75 after:border-x-transparent after:border-b-transparent">Không thích</div>
</button>
<!---->
</div>
</div>
<!---->
</div>
</div>
</div>
<!---->
</div>
</div>
<!---->
</div>
</div>
</div>
<!---->
<!---->
</div>
</div>
<div class="px-4 duration-200 pb-0">
<div class="flex flex-col items-end py-4 space-y-2">
<div class="quick-prompt-tag active">Giới thiệu về bệnh viện</div>
<div class="quick-prompt-tag active">Đặt lịch khám</div>
<div class="quick-prompt-tag active">Thủ tục khám bệnh bảo hiểm y tế</div>
</div>
</div>
</div>
<div class="relative">
<!---->
<!---->
<div class="relative z-30 bg-white">
<div class="w-full px-3 pt-1">
<div class="border border-gray-200 rounded-2xl">
<!---->
<!---->
<!---->
<div class="relative flex w-full">
<div class="flex absolute top-1/2 -translate-y-1/2 left-1.5 z-10 items-center quick-prompts">
<button type="button" class="flex items-center justify-center transition-colors rounded-full size-8 overflow-hidden hover:bg-gray-50 bg-white" title="Gợi ý nhanh">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" size="18" class="lucide lucide-menu-icon text-gray-500">
<line x1="4" x2="20" y1="12" y2="12"></line>
<line x1="4" x2="20" y1="6" y2="6"></line>
<line x1="4" x2="20" y1="18" y2="18"></line>
</svg>
</button>
<!---->
</div>
<textarea id="eac-chat-input" class="resize-none rounded-3xl input" placeholder="Nhập tin nhắn..." rows="1" style="height: 44px;"></textarea>
<div class="absolute z-10 flex items-center duration-200 top-1/2 -translate-y-1/2 right-1">
<!---->
<div class="w-fit relative group">
<input type="file" class="hidden" accept=".pdf,.doc,.docx,.xls,.xlsx,.csv,.jpg,.jpeg,.png,.webp,.bmp,.tiff,.tif,.heic,.avif,.mp3,.wav,.ogg,.m4a" multiple="">
<button type="button" class="size-9 flex items-center justify-center rounded-full sm:hover:bg-gray-100 cursor-pointer text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" size="18" class="lucide lucide-paperclip-icon">
<path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"></path>
</svg>
</button>
<!---->
</div>
<button type="button" class="bg-primary text-white sm:hover:opacity-80 flex items-center justify-center ml-1.5 duration-200 rounded-full size-9">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" size="16" class="lucide lucide-send-horizontal-icon">
<path d="m3 3 3 9-3 9 19-9Z"></path>
<path d="M6 12h16"></path>
</svg>
</button>
</div>
</div>
<!---->
</div>
</div>
</div>
</div>
<div class="drawer-form" tabindex="-1"></div>
<div class="drawer-form" tabindex="-1"></div>
<!---->
<div class="drawer-overlay-custom"></div>
<!---->
<div class="drawer-overlay-custom"></div>
<!---->
<div class="drawer-overlay-custom"></div>
<!---->
<div class="drawer-overlay-custom"></div>
<!---->
</div>
<!---->
<!---->
</div>
<!---->
<div class="flex items-center justify-center w-full py-1 flex-row bg-white">
<!---->
<a href="https://geteasy.ai/" target="_blank" rel="nofollow noreferrer" class="text-[10px] text-gray-500 font-medium flex items-center space-x-1 px-2 pb-0.5">
<span>Powered by</span>
<div class="flex items-center space-x-1">
<svg width="20" height="6.25" viewBox="0 0 32 10" fill="none" xmlns="http://www.w3.org/2000/svg" class="mt-px">
<path d="M29.0312 2.39062V2.39062C21.8342 9.58768 10.1655 9.58768 2.9684 2.39062V2.39062" stroke="url(#paint0_linear_950_398)" stroke-width="3" stroke-linecap="square"></path>
<defs>
<lineargradient id="paint0_linear_950_398" x1="9.48412" y1="8.90634" x2="22.5155" y2="-4.12509" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#3A087F"></stop>
<stop offset="40%" stop-color="#A535EB"></stop>
<stop offset="70%" stop-color="#FCA63E"></stop>
<stop offset="88%" stop-color="#54D5A4"></stop>
<stop offset="100%" stop-color="#1B8AF7"></stop>
</lineargradient>
</defs>
</svg>
<span class="font-bold text-gray-900">Easy AI</span>
</div>
</a>
</div>
</div>
</div>
<div class="z-[var(--z-index-bubblechat)] chat-button-group fixed bottom-[var(--ui-alignment-offset-vertical)] space-y-2">
<div>
<div class="flex-col space-y-2 sm:space-y-3 group items-end hidden sm:flex">
<div class="pr-0.5 duration-500 group-hover:block inner-close hidden">
<button class="flex overflow-hidden relative justify-center items-center w-6 h-6 md:w-5 md:h-5 rounded-full hover:opacity-70">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="relative z-10 w-4 md:w-3 text-white">
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
<div class="absolute inset-0 bg-gray-500/60"></div>
</button>
</div>
<div class="translate-y-0 opacity-100 visible px-3 pt-2 cursor-pointer pb-1 box-shadow-md bg-white rounded-xl relative w-[180px] sm:w-[200px] duration-1000 delay-75">
<div class="items-center space-x-1 sm:space-x-2 mb-1 sm:mb-1.5 hidden sm:flex">
<div class="overflow-hidden size-5 sm:size-6 shrink-0 rounded">
<img src="https://assets.geteasy.ai/workspaces/2353/bots/2770/public/0fd2dbe4-63bf-4d8e-871f-ab903f800fca.png" alt="Trợ lý AI của UMC" class="object-cover w-full h-full">
</div>
<div class="text-sm font-medium text-primary leading-[120%]">Trợ lý AI của UMC</div>
</div>
<div>
<div data-v-62f66be8="" class="vertical-custom-slide duration-500" style="height: 42px;">
<div data-v-62f66be8="" class="duration-1000 vertical-move" style="transform: translateY(-42px);">
<div data-v-62f66be8="" class="flex-1 text-sm text-gray-700 duration-1000 vertical-slide-item opacity-0">Xin chào, Em là trợ lý ảo của UMC 👋</div>
<div data-v-62f66be8="" class="flex-1 text-sm text-gray-700 duration-1000 vertical-slide-item opacity-100">Em rất sẵn lòng hỗ trợ Anh/Chị 😊</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!---->
<!---->
<div class="duration-300 relative w-fit border-[3px] rounded-lg delay-300 ml-auto border-transparent">
<!---->
<button aria-label="chat-button" class="flex relative justify-center items-center cursor-pointer chat-button disabled:pointer-events-none disabled:opacity-50 group rounded-lg bg-transparent shadow overflow-hidden">
<img src="https://assets.geteasy.ai/workspaces/2353/bots/2770/public/5026d034-feea-46db-a0c1-60c078ef5d5d.png" alt="Trợ lý AI của UMC" class="object-cover w-full h-full">
</button>
</div>
</div>
</div>
<!---->
<!---->
<!---->
<!---->
<!---->
</div>
</div>
<link rel="stylesheet" href="https://widget.geteasy.ai/dist/widget/main.css?v=1784950551115">
</template>
</div>
</div>
<th:block layout:fragment="scripts">
<!-- Swiper JS & Initialization -->
<script th:src="@{/js/swiper-bundle.min.js}"></script>
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<script>
@@ -679,10 +333,10 @@
return;
}
const sliderConfigs = [{
slug: 'gallery',
prev: '.btn-navigation-gallery-prev',
next: '.btn-navigation-gallery-next',
pagination: '.swiper-pagination-training-gallery'
slug: 'dao-tao-01',
prev: '.btn-navigation-dao-tao-01-prev',
next: '.btn-navigation-dao-tao-01-next',
pagination: ''
}, {
slug: 'khoa-hoc',
prev: '.btn-navigation-khoa-hoc-prev',
@@ -711,10 +365,17 @@
}];
sliderConfigs.forEach(function(config) {
const el = document.querySelector('.swiper-' + config.slug);
if (el && !el.classList.contains('swiper-initialized')) {
if (el) {
if (el.swiper) {
el.swiper.destroy(true, true);
}
new Swiper('.swiper-' + config.slug, {
slidesPerView: 'auto',
spaceBetween: 16,
observer: true,
observeParents: true,
watchOverflow: false,
rewind: true,
navigation: {
nextEl: config.next,
prevEl: config.prev,
@@ -734,6 +395,13 @@
}
window.addEventListener('load', initDaoTaoSliders);
</script>
</div>
<th:block layout:fragment="scripts">
<script>
if (typeof initDaoTaoSliders === 'function') {
initDaoTaoSliders();
}
</script>
</th:block>
</body>
</html>
@@ -156,10 +156,11 @@
</div>
</div>
<div th:utext="${doctorIntroHtml}"></div>
<div style="display: flex; flex-direction: row; justify-content: start; align-items: center; gap: var(--s2);" class="mb-6 lg:mt-6">
<button type="button" class="btn space-x-2 text-center label-3 w-[286px] !py-2 disabled:opacity-50 disabled:cursor-not-allowed text-white hover:opacity-90 transition-opacity" th:data-url="${doctor.bookingUrl != null and !doctor.bookingUrl.isEmpty() ? doctor.bookingUrl : '#'}" onclick="window.location.href=this.getAttribute('data-url')">
<div>Đặt lịch khám</div>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<a href="tel:18001115" style="color: white; text-decoration: none; width: 100%; display: flex; flex-direction: row; gap: 10px; justify-content: center; align-items: center;">Đặt lịch khám
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_83_297)">
<path d="M0 16.26C0 17.3 0.366667 18.18 1.1 18.9C1.83333 19.62 2.72 19.9867 3.76 20H16.26C17.2867 20 18.1667 19.6333 18.9 18.9C19.6333 18.1667 20 17.2867 20 16.26V3.76C20 2.94667 19.7667 2.22 19.3 1.58C18.8333 0.94 18.2333 0.493333 17.5 0.24V1.88C17.5 2.4 17.32 2.84667 16.96 3.22C16.6 3.59333 16.1533 3.77333 15.62 3.76C15.0867 3.74667 14.6467 3.56667 14.3 3.22C13.9533 2.87333 13.7733 2.42667 13.76 1.88V0H6.26V1.88C6.26 2.4 6.07333 2.84667 5.7 3.22C5.32667 3.59333 4.88667 3.77333 4.38 3.76C3.87333 3.74667 3.42667 3.56667 3.04 3.22C2.65333 2.87333 2.47333 2.42667 2.5 1.88V0.24C1.76667 0.506667 1.16667 0.953333 0.7 1.58C0.233333 2.20667 0 2.93333 0 3.76L0 16.26ZM2.5 16.26V6.26H17.5V16.26C17.5 16.6067 17.38 16.9 17.14 17.14C16.9 17.38 16.6067 17.5 16.26 17.5H3.76C3.41333 17.5 3.11333 17.38 2.86 17.14C2.60667 16.9 2.48667 16.6067 2.5 16.26ZM3.76 1.88C3.76 2.05333 3.82 2.2 3.94 2.32C4.06 2.44 4.20667 2.5 4.38 2.5C4.55333 2.5 4.7 2.44 4.82 2.32C4.94 2.2 5 2.05333 5 1.88V0H3.76V1.88ZM5 15H7.5V12.5H5V15ZM5 11.26H7.5V8.76H5V11.26ZM8.76 15H11.26V12.5H8.76V15ZM8.76 11.26H11.26V8.76H8.76V11.26ZM12.5 15H15V12.5H12.5V15ZM12.5 11.26H15V8.76H12.5V11.26ZM15 1.88C15 2.05333 15.06 2.2 15.18 2.32C15.3 2.44 15.4467 2.5 15.62 2.5C15.7933 2.5 15.94 2.44 16.06 2.32C16.18 2.2 16.2467 2.05333 16.26 1.88V0H15V1.88Z" fill="white"></path>
</g>
@@ -169,19 +170,14 @@
</clipPath>
</defs>
</svg>
</a>
</button>
<button type="button" class="btn space-x-2 text-center label-3 w-[286px] !py-2 disabled:opacity-50 disabled:cursor-not-allowed text-white hover:opacity-90 transition-opacity" th:data-url="${doctor.bookingUrl != null and !doctor.bookingUrl.isEmpty() ? doctor.bookingUrl : '#'}" onclick="window.location.href=this.getAttribute('data-url')">
<div>Đặt lịch khám</div>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_83_297)">
<path d="M0 16.26C0 17.3 0.366667 18.18 1.1 18.9C1.83333 19.62 2.72 19.9867 3.76 20H16.26C17.2867 20 18.1667 19.6333 18.9 18.9C19.6333 18.1667 20 17.2867 20 16.26V3.76C20 2.94667 19.7667 2.22 19.3 1.58C18.8333 0.94 18.2333 0.493333 17.5 0.24V1.88C17.5 2.4 17.32 2.84667 16.96 3.22C16.6 3.59333 16.1533 3.77333 15.62 3.76C15.0867 3.74667 14.6467 3.56667 14.3 3.22C13.9533 2.87333 13.7733 2.42667 13.76 1.88V0H6.26V1.88C6.26 2.4 6.07333 2.84667 5.7 3.22C5.32667 3.59333 4.88667 3.77333 4.38 3.76C3.87333 3.74667 3.42667 3.56667 3.04 3.22C2.65333 2.87333 2.47333 2.42667 2.5 1.88V0.24C1.76667 0.506667 1.16667 0.953333 0.7 1.58C0.233333 2.20667 0 2.93333 0 3.76L0 16.26ZM2.5 16.26V6.26H17.5V16.26C17.5 16.6067 17.38 16.9 17.14 17.14C16.9 17.38 16.6067 17.5 16.26 17.5H3.76C3.41333 17.5 3.11333 17.38 2.86 17.14C2.60667 16.9 2.48667 16.6067 2.5 16.26ZM3.76 1.88C3.76 2.05333 3.82 2.2 3.94 2.32C4.06 2.44 4.20667 2.5 4.38 2.5C4.55333 2.5 4.7 2.44 4.82 2.32C4.94 2.2 5 2.05333 5 1.88V0H3.76V1.88ZM5 15H7.5V12.5H5V15ZM5 11.26H7.5V8.76H5V11.26ZM8.76 15H11.26V12.5H8.76V15ZM8.76 11.26H11.26V8.76H8.76V11.26ZM12.5 15H15V12.5H12.5V15ZM12.5 11.26H15V8.76H12.5V11.26ZM15 1.88C15 2.05333 15.06 2.2 15.18 2.32C15.3 2.44 15.4467 2.5 15.62 2.5C15.7933 2.5 15.94 2.44 16.06 2.32C16.18 2.2 16.2467 2.05333 16.26 1.88V0H15V1.88Z" fill="white"></path>
</g>
<defs>
<clipPath id="clip0_83_297">
<rect width="20" height="20" fill="white"></rect>
</clipPath>
</defs>
</svg>
<a href="tel:18001115" style="color: white; text-decoration: none; width: 100%; display: flex; flex-direction: row; gap: 10px; justify-content: center; align-items: center;">Liên hệ
<svg class="text-white" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
</a>
</button>
</div>
</div>
@@ -576,23 +572,24 @@
<div class="jam-accordion-content">
<div class="prose doctor-info-prose transition-all duration-200 !text-[14px] space-y-2 xl:m-3 m-2" th:utext="${doctor.workExperience}"></div>
</div>
<div class="jam-accordion active" th:if="${doctor.achievements != null and !doctor.achievements.isEmpty()}">
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="bg-primary-600 xl:p-3 p-2 flex justify-between items-center cursor-pointer jam-accordion-title" tabindex="0" aria-expanded="true" aria-controls="association-content">
<div class="title-3 text-white">Hiệp hội chuyên môn &amp; Thành tựu</div>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down w-4 h-4 text-white duration-150 -rotate-180">
<path d="m6 9 6 6 6-6"></path>
</svg>
</div>
<div class="jam-accordion-content">
<div class="prose doctor-info-prose transition-all duration-200 !text-[14px] xl:m-3 m-2" th:utext="${doctor.achievements}"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jam-accordion active" th:if="${doctor.achievements != null and !doctor.achievements.isEmpty()}">
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="bg-primary-600 xl:p-3 p-2 flex justify-between items-center cursor-pointer jam-accordion-title" tabindex="0" aria-expanded="true" aria-controls="association-content">
<div class="title-3 text-white">Hiệp hội chuyên môn &amp; Thành tựu</div>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down w-4 h-4 text-white duration-150 -rotate-180">
<path d="m6 9 6 6 6-6"></path>
</svg>
</div>
<div class="jam-accordion-content">
<div class="prose doctor-info-prose transition-all duration-200 !text-[14px] xl:m-3 m-2" th:utext="${doctor.achievements}"></div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -602,5 +599,38 @@
<!--/$-->undefined
<!--$-->undefined
<!--/$-->undefined </main>undefined</div>undefined
<th:block layout:fragment="scripts">
<script th:inline="javascript">
/*<![CDATA[*/
document.addEventListener('DOMContentLoaded', function() {
var doctorCode = /*[[${doctor.doctorCode}]]*/ 'default';
if (doctorCode && doctorCode !== 'default') {
const container = document.getElementById('schedule-' + doctorCode);
if (container) {
fetch('/api/public/doctor-schedule/' + doctorCode)
.then(response => response.json())
.then(data => {
if (data && data.length > 0) {
let html = '<ul class="pl-0 mb-0 list-unstyled space-y-2">';
data.forEach(day => {
let dayText = day.dayOfWeek === 1 ? 'Chủ nhật' : 'Thứ ' + day.dayOfWeek;
let dateText = day.date ? ' <span class="text-gray-500">(' + day.date + ')</span>' : '';
html += '<li class="flex items-center gap-2"><i class="fas fa-check-circle text-green-500"></i> <strong>' + dayText + '</strong>' + dateText + '</li>';
});
html += '</ul>';
container.innerHTML = html;
} else {
container.innerHTML = '<span class="text-gray-500 italic">Bác sĩ chưa có lịch khám tuần này</span>';
}
})
.catch(err => {
container.innerHTML = '<span class="text-red-500">Lỗi tải dữ liệu lịch khám</span>';
});
}
}
});
/*]]>*/
</script>
</th:block>
</body>undefined
</html>
@@ -55,11 +55,11 @@
</div>
</div>
<div class="xl:px-6 px-4 xl:pb-6 pb-4 xl:mt-3 mt-2 flex items-center justify-between gap-x-2">
<a class="btn btn-outline-primary !inline-flex w-[149px] text-center !px-0 group items-center justify-center gap-1.5" th:href="@{/dat-lich}">
<a class="btn btn-outline-primary !inline-flex w-[149px] text-center !px-0 group items-center justify-center gap-1.5" style="text-decoration: none;" th:href="@{/bac-si/{code}(code=${doc.doctorCode})}">
<div class="text-white">Đặt lịch khám</div>
<svg class="text-white" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="4" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/><path d="M8 14h.01"/><path d="M12 14h.01"/><path d="M16 14h.01"/><path d="M8 18h.01"/><path d="M12 18h.01"/><path d="M16 18h.01"/></svg>
</a>
<a class="btn btn-outline-primary !inline-flex w-[149px] text-center !px-0 group items-center justify-center gap-1.5" href="tel:18001115">
<a class="btn btn-outline-primary !inline-flex w-[149px] text-center !px-0 group items-center justify-center gap-1.5" style="text-decoration: none;" href="tel:18001115">
<div class="text-white">Liên hệ</div>
<svg class="text-white" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
</a>
@@ -67,6 +67,35 @@
placeholder="e.g. BS. CKII">
</div>
<!-- Position -->
<div class="form-group">
<label for="doctorPosition" class="font-weight-bold">Position</label>
<input type="text" class="form-control" id="doctorPosition" th:field="*{position}"
placeholder="e.g. Trưởng khoa">
</div>
<!-- Gender -->
<div class="form-group">
<label for="doctorGender" class="font-weight-bold">Gender</label>
<select class="form-control" id="doctorGender" th:field="*{gender}">
<option value="">— Select Gender —</option>
<option value="nam">Nam</option>
<option value="nữ">Nữ</option>
</select>
</div>
<!-- Phone -->
<div class="form-group">
<label for="doctorPhone" class="font-weight-bold">Phone Number</label>
<input type="text" class="form-control" id="doctorPhone" th:field="*{phoneNumber}">
</div>
<!-- Email -->
<div class="form-group">
<label for="doctorEmail" class="font-weight-bold">Email</label>
<input type="email" class="form-control" id="doctorEmail" th:field="*{email}">
</div>
<!-- Specialty -->
<div class="form-group">
<label for="specialtyId" class="font-weight-bold">Specialty</label>
@@ -122,6 +151,14 @@
placeholder="https://booking.bvdaihoc.com.vn/...">
</div>
<!-- Active (Show on Web) -->
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="doctorActive" th:field="*{active}">
<label class="custom-control-label font-weight-bold" for="doctorActive">Show on Web (Active)</label>
</div>
</div>
<!-- Buttons -->
<div class="d-flex justify-content-between mt-4">
<a th:if="${!isNew}" th:href="@{/manage/doctors}" class="btn btn-secondary btn-sm">
@@ -156,11 +193,12 @@
<thead class="thead-light">
<tr>
<th width="5%">ID</th>
<th width="30%">Name</th>
<th width="15%">Title</th>
<th width="20%">Specialty</th>
<th width="10%">Active</th>
<th width="5%">Booking</th>
<th width="20%">Name</th>
<th width="15%">Title/Position</th>
<th width="10%">Gender</th>
<th width="15%">Contact</th>
<th width="15%">Specialty</th>
<th width="5%">Active</th>
<th width="15%">Actions</th>
</tr>
</thead>
@@ -171,7 +209,13 @@
<strong th:text="${doc.name}"></strong>
</td>
<td>
<span th:text="${doc.title}"></span>
<span th:text="${doc.title}"></span><br>
<small class="text-muted" th:text="${doc.position}"></small>
</td>
<td th:text="${doc.gender}"></td>
<td>
<small th:if="${doc.phoneNumber != null && !doc.phoneNumber.isEmpty()}" th:text="${doc.phoneNumber}"></small><br th:if="${doc.phoneNumber != null && !doc.phoneNumber.isEmpty()}">
<small th:if="${doc.email != null && !doc.email.isEmpty()}" th:text="${doc.email}"></small>
</td>
<td>
<span th:if="${doc.specialty != null}" th:text="${doc.specialty.name}" class="badge badge-info"></span>
@@ -181,10 +225,6 @@
<span th:if="${doc.active}" class="badge badge-success">Active</span>
<span th:if="${!doc.active}" class="badge badge-secondary">Inactive</span>
</td>
<td>
<a th:if="${doc.bookingUrl != null && !doc.bookingUrl.isEmpty()}" th:href="${doc.bookingUrl}" target="_blank" class="badge badge-success">Link</a>
<span th:if="${doc.bookingUrl == null || doc.bookingUrl.isEmpty()}" class="text-muted"></span>
</td>
<td class="text-center">
<a th:href="@{/manage/doctors/{id}/edit(id=${doc.id})}"
class="btn btn-sm btn-outline-info mr-1" title="Edit">
@@ -60,5 +60,7 @@
<script th:src="@{/js/ambient-video.js}"></script>
<!-- wp_footer hook -->
<th:block th:utext="${hookManager.doActionAndReturn('wp_footer')}"></th:block>
<!-- Page Scripts Fragment -->
<th:block layout:fragment="scripts"></th:block>
</body>
</html>