-
-
-

+
+
+
+
+

+
+
+
BS CKI.
+
Tăng Tuấn Phong
+
Phó Trưởng Khoa Cấp cứu
+
-
-
- CHỨNG CHỈ ĐÀO TẠO KỸ THUẬT CHUYÊN MÔN: KỸ...
-
-
-
- Thăm dò chức năng hô hấp là một phần quan trọng trong chẩn đoán, điều trị và theo dõi các bệnh lý hô...
-
-
-
-
-
-
+
-
-
+
+
-
-
-

+
+
+
+
+

+
+
+
TS BS.
+
Nguyễn Tuấn Anh
+
Khoa Cấp cứu
+
-
-
- CHƯƠNG TRÌNH CHỨNG CHỈ ĐÀO TẠO KỸ THUẬT...
-
-
-
- Đột quỵ là bệnh phổ biến với khoảng 200.000 ca đột quỵ mới mỗi năm, theo thống kê của Bộ Y tế. Hơn thế...
-
-
-
-
-
-
+
-
+
+
+
+
diff --git a/BV_DHYD_HCM/temp.html b/BV_DHYD_HCM/temp.html
index 939b7ad8..107d8ce1 100644
--- a/BV_DHYD_HCM/temp.html
+++ b/BV_DHYD_HCM/temp.html
@@ -1,166 +1,382 @@
-
-
-
-
-
-
Chia sẻ lời tri ân
-
-
-
-
-
-
+
+
+
Đội ngũ nhân sự
-
-
- Chia sẻ lời tri ân
-
-
-
-
+
+
+
- Gửi thông tin
-
+

+
+
+

+
+
-
+
+
+
+
+
+

+
+
+

+
+
+
+
+
-
-
+
+
+
+
+
diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageMenuController.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageMenuController.java
index c3e8e42e..79547fb6 100644
--- a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageMenuController.java
+++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageMenuController.java
@@ -51,13 +51,14 @@ public class ManageMenuController {
}
@PostMapping("/{id}/items/add")
- public String addMenuItem(@PathVariable Long id, @RequestParam String label, @RequestParam String url, @RequestParam(required = false) Long parentId, @RequestParam(required = false) String title, @RequestParam(required = false) String imageUrl, RedirectAttributes redirectAttributes) {
+ public String addMenuItem(@PathVariable Long id, @RequestParam String label, @RequestParam String url, @RequestParam(required = false) Long parentId, @RequestParam(required = false) String title, @RequestParam(required = false) String imageUrl, @RequestParam(required = false) String description, RedirectAttributes redirectAttributes) {
Menu menu = menuService.findById(id).orElseThrow(() -> new IllegalArgumentException("Invalid menu Id"));
MenuItem item = new MenuItem();
item.setLabel(label);
item.setUrl(url);
item.setTitle(title);
item.setImageUrl(imageUrl);
+ item.setDescription(description);
item.setMenu(menu);
item.setDisplayOrder(menu.getItems().size());
@@ -94,12 +95,13 @@ public class ManageMenuController {
}
@PostMapping("/{id}/items/{itemId}/update")
- public String updateMenuItem(@PathVariable Long id, @PathVariable Long itemId, @RequestParam String label, @RequestParam String url, @RequestParam(required = false) Long parentId, @RequestParam(required = false) Integer displayOrder, @RequestParam(required = false) String title, @RequestParam(required = false) String imageUrl, RedirectAttributes redirectAttributes) {
+ public String updateMenuItem(@PathVariable Long id, @PathVariable Long itemId, @RequestParam String label, @RequestParam String url, @RequestParam(required = false) Long parentId, @RequestParam(required = false) Integer displayOrder, @RequestParam(required = false) String title, @RequestParam(required = false) String imageUrl, @RequestParam(required = false) String description, RedirectAttributes redirectAttributes) {
MenuItem item = menuItemRepository.findById(itemId).orElseThrow(() -> new IllegalArgumentException("Invalid item Id"));
item.setLabel(label);
item.setUrl(url);
item.setTitle(title);
item.setImageUrl(imageUrl);
+ item.setDescription(description);
if (displayOrder != null) {
item.setDisplayOrder(displayOrder);
}
diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/MenuItem.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/MenuItem.java
index 144b867c..bf55a2e1 100644
--- a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/MenuItem.java
+++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/MenuItem.java
@@ -51,6 +51,10 @@ public class MenuItem extends AbstractAuditingEntity
{
@Column(name = "image_url", length = 500)
private String imageUrl;
+ @Size(max = 1000)
+ @Column(name = "description", length = 1000)
+ private String description;
+
@Column(name = "display_order")
private Integer displayOrder = 0;
@@ -127,6 +131,14 @@ public class MenuItem extends AbstractAuditingEntity {
this.imageUrl = imageUrl;
}
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/sisvietnamvn_main/src/main/resources/config/liquibase/changelog/20260811031000_add_description_to_menu_item.xml b/sisvietnamvn_main/src/main/resources/config/liquibase/changelog/20260811031000_add_description_to_menu_item.xml
new file mode 100644
index 00000000..8d28b6b7
--- /dev/null
+++ b/sisvietnamvn_main/src/main/resources/config/liquibase/changelog/20260811031000_add_description_to_menu_item.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sisvietnamvn_main/src/main/resources/config/liquibase/master.xml b/sisvietnamvn_main/src/main/resources/config/liquibase/master.xml
index 8ae17e11..ed82f7ee 100644
--- a/sisvietnamvn_main/src/main/resources/config/liquibase/master.xml
+++ b/sisvietnamvn_main/src/main/resources/config/liquibase/master.xml
@@ -59,4 +59,5 @@
+
diff --git a/sisvietnamvn_main/src/main/resources/static/css/custom.css b/sisvietnamvn_main/src/main/resources/static/css/custom.css
index 2b9c1326..35ea8e37 100644
--- a/sisvietnamvn_main/src/main/resources/static/css/custom.css
+++ b/sisvietnamvn_main/src/main/resources/static/css/custom.css
@@ -660,6 +660,114 @@ figure.table table tr:hover {
height: stretch;
}
+.card-icon-shadow {
+ display: none;
+}
+
+/* ==========================================================================
+ Image Alignment Rules (Rich Text / Editor / CMS Content)
+ Supports display: inline-block !important in element.style
+ ========================================================================== */
+
+/* 1. Force parent elements containing centered images to text-align: center !important */
+p[style*="text-align: center"],
+p[style*="text-align:center"],
+div[style*="text-align: center"],
+div[style*="text-align:center"],
+figure[style*="text-align: center"],
+figure[style*="text-align:center"],
+section[style*="text-align: center"],
+section[style*="text-align:center"],
+.text-center,
+.ce-block--align-center {
+ text-align: center !important;
+}
+
+/* 2. Parent containers auto-center if child img has auto-margin or center styles in element.style */
+p:has(> img[style*="margin-left: auto"]),
+p:has(> img[style*="margin-left:auto"]),
+div:has(> img[style*="margin-left: auto"]),
+div:has(> img[style*="margin-left:auto"]),
+figure:has(> img[style*="margin-left: auto"]),
+figure:has(> img[style*="margin-left:auto"]),
+p:has(> img[style*="text-align: center"]),
+p:has(> img[style*="text-align:center"]),
+div:has(> img[style*="text-align: center"]),
+div:has(> img[style*="text-align:center"]),
+p:has(> img.aligncenter),
+div:has(> img.aligncenter),
+figure:has(> img.aligncenter) {
+ text-align: center !important;
+}
+
+/* 3. Center Aligned Images (Works for both block and inline-block) */
+p[style*="text-align: center"] img,
+p[style*="text-align:center"] img,
+div[style*="text-align: center"] img,
+div[style*="text-align:center"] img,
+figure[style*="text-align: center"] img,
+figure[style*="text-align:center"] img,
+section[style*="text-align: center"] img,
+section[style*="text-align:center"] img,
+[style*="text-align: center"] > img,
+[style*="text-align:center"] > img,
+img[style*="text-align: center"],
+img[style*="text-align:center"],
+img[style*="margin-left: auto"][style*="margin-right: auto"],
+img[style*="margin-left:auto"][style*="margin-right:auto"],
+img.aligncenter,
+figure.aligncenter,
+figure.aligncenter img,
+figure.image-style-align-center,
+figure.image-style-align-center img,
+.text-center img,
+.text-center > img,
+p.text-center img,
+div.text-center img {
+ margin-left: auto !important;
+ margin-right: auto !important;
+}
+
+/* Left Aligned Images */
+p[style*="text-align: left"] img,
+p[style*="text-align:left"] img,
+div[style*="text-align: left"] img,
+div[style*="text-align:left"] img,
+figure[style*="text-align: left"] img,
+figure[style*="text-align:left"] img,
+[style*="text-align: left"] > img,
+[style*="text-align:left"] > img,
+img[style*="text-align: left"],
+img[style*="text-align:left"],
+img.alignleft,
+figure.alignleft,
+figure.alignleft img,
+.text-left img,
+.text-left > img {
+ margin-left: 0 !important;
+ margin-right: auto !important;
+}
+
+/* Right Aligned Images */
+p[style*="text-align: right"] img,
+p[style*="text-align:right"] img,
+div[style*="text-align: right"] img,
+div[style*="text-align:right"] img,
+figure[style*="text-align: right"] img,
+figure[style*="text-align:right"] img,
+[style*="text-align: right"] > img,
+[style*="text-align:right"] > img,
+img[style*="text-align: right"],
+img[style*="text-align:right"],
+img.alignright,
+figure.alignright,
+figure.alignright img,
+.text-right img,
+.text-right > img {
+ margin-left: auto !important;
+ margin-right: 0 !important;
+}
+
/* ==========================================================================
5. Responsive Breakpoints — 3 Groups Only
========================================================================== */
diff --git a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-config.js b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-config.js
index d772b70e..565c6fbf 100644
--- a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-config.js
+++ b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-config.js
@@ -325,13 +325,8 @@ class SISAccordionTool {
}
-
-
-
// SISHeroBannerTool extracted to /js/manage/editor-plugins/hero-banner.js
-
-
/**
* Initialize the SIS Block Editor on a given holder element.
*
@@ -457,7 +452,22 @@ function initSISEditor(holderId, hiddenInputId, initialData, skipSubmitHandler =
if (Object.prototype.hasOwnProperty.call(sourceTools, key)) {
var toolDef = sourceTools[key];
if (toolDef) {
- allTools[key] = toolDef;
+ var toolClass = toolDef.class || toolDef;
+ if (typeof toolClass === 'function') {
+ if (registeredClasses.has(toolClass)) {
+ // Mark alias keys with toolbox: false so Editor.js parses the block type without adding duplicate menu items
+ if (typeof toolDef === 'function') {
+ allTools[key] = { class: toolDef, toolbox: false };
+ } else {
+ allTools[key] = Object.assign({}, toolDef, { toolbox: false });
+ }
+ } else {
+ registeredClasses.add(toolClass);
+ allTools[key] = toolDef;
+ }
+ } else {
+ allTools[key] = toolDef;
+ }
}
}
}
diff --git a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/human-resource.js b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/human-resource.js
new file mode 100644
index 00000000..e2c143b4
--- /dev/null
+++ b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/human-resource.js
@@ -0,0 +1,418 @@
+/**
+ * SISHumanResourceTool — Dedicated Human Resources / Doctor List Block Tool for Editor.js
+ * Allows content managers to easily manage doctor cards, degrees, names, positions, avatars, specialty icons, and booking/profile action links.
+ */
+(function () {
+ 'use strict';
+
+ class SISHumanResourceTool {
+ static get toolbox() {
+ return {
+ title: 'Đội ngũ nhân sự / Bác sĩ',
+ icon: ''
+ };
+ }
+
+ constructor({ data, api, readOnly }) {
+ this.api = api;
+ this.readOnly = readOnly;
+ this.instanceId = 'hr_' + Math.random().toString(36).substring(7);
+
+ this.data = {
+ title: (data && data.title !== undefined) ? data.title : 'Đội ngũ nhân sự',
+ cols: parseInt(data && data.cols) || 3,
+ globalClass: (data && data.globalClass) || '',
+ globalId: (data && data.globalId) || '',
+ globalAttributes: (data && data.globalAttributes) || (data && data.attributes) || '',
+ showMoreBtn: (data && (data.showMoreBtn === true || data.showMoreBtn === 'true')),
+ showMoreText: (data && data.showMoreText) || 'Xem thêm',
+ showMoreUrl: (data && data.showMoreUrl) || '#',
+ items: (data && data.items && Array.isArray(data.items)) ? data.items : []
+ };
+
+ // Populate default items if empty
+ if (this.data.items.length === 0) {
+ this.data.items.push({
+ degree: 'BS CKII.',
+ name: 'Nguyễn Viết Hậu',
+ position: 'Trưởng Khoa Cấp cứu',
+ avatarUrl: 'https://images.unsplash.com/photo-1622253692010-333f2da6031d?auto=format&fit=crop&w=300&q=80',
+ specialtyIconUrl: '',
+ profileUrl: '#',
+ profileBtnText: 'Xem hồ sơ',
+ bookingUrl: '#',
+ bookingBtnText: 'Đặt lịch khám',
+ active: true
+ });
+ }
+ this.wrapper = undefined;
+ }
+
+ render() {
+ this.wrapper = document.createElement('div');
+ this.wrapper.className = 'p-3 bg-light border rounded mb-3 ce-hr-tool-wrapper';
+ this.wrapper.style.fontFamily = 'inherit';
+
+ const titleLabel = document.createElement('div');
+ titleLabel.className = 'font-weight-bold text-primary small mb-3 d-flex align-items-center justify-content-between';
+ titleLabel.innerHTML = ' Quản lý Đội ngũ Nhân sự / Danh sách Bác sĩ';
+ this.wrapper.appendChild(titleLabel);
+
+ // Global Block Settings
+ const settingsRow = document.createElement('div');
+ settingsRow.className = 'form-row mb-3 pb-2 border-bottom';
+
+ // 1. Block Title
+ const titleDiv = document.createElement('div');
+ titleDiv.className = 'col-md-4 mb-2';
+ titleDiv.innerHTML = '';
+ this.titleInput = document.createElement('input');
+ this.titleInput.type = 'text';
+ this.titleInput.className = 'form-control form-control-sm';
+ this.titleInput.placeholder = 'Đội ngũ nhân sự...';
+ this.titleInput.value = this.data.title;
+ titleDiv.appendChild(this.titleInput);
+
+ // 2. Columns Select
+ const colDiv = document.createElement('div');
+ colDiv.className = 'col-md-2 mb-2';
+ colDiv.innerHTML = '';
+ this.colsSelect = document.createElement('select');
+ this.colsSelect.className = 'form-control form-control-sm';
+ [1, 2, 3, 4].forEach(num => {
+ const opt = document.createElement('option');
+ opt.value = num;
+ opt.textContent = num + ' cột';
+ if (num === this.data.cols) opt.selected = true;
+ this.colsSelect.appendChild(opt);
+ });
+ colDiv.appendChild(this.colsSelect);
+
+ // 3. CSS Class & Global ID
+ const cssDiv = document.createElement('div');
+ cssDiv.className = 'col-md-3 mb-2';
+ cssDiv.innerHTML = '';
+ this.globalClassInput = document.createElement('input');
+ this.globalClassInput.type = 'text';
+ this.globalClassInput.className = 'form-control form-control-sm';
+ this.globalClassInput.placeholder = 'sis-doctor-section';
+ this.globalClassInput.value = this.data.globalClass;
+ cssDiv.appendChild(this.globalClassInput);
+
+ const idDiv = document.createElement('div');
+ idDiv.className = 'col-md-3 mb-2';
+ idDiv.innerHTML = '';
+ this.globalIdInput = document.createElement('input');
+ this.globalIdInput.type = 'text';
+ this.globalIdInput.className = 'form-control form-control-sm';
+ this.globalIdInput.placeholder = 'doctor-list-section';
+ this.globalIdInput.value = this.data.globalId;
+ idDiv.appendChild(this.globalIdInput);
+
+ settingsRow.appendChild(titleDiv);
+ settingsRow.appendChild(colDiv);
+ settingsRow.appendChild(cssDiv);
+ settingsRow.appendChild(idDiv);
+ this.wrapper.appendChild(settingsRow);
+
+ // Show More Button Settings Row
+ const moreRow = document.createElement('div');
+ moreRow.className = 'form-row mb-3 pb-2 border-bottom align-items-center';
+
+ const showMoreToggleDiv = document.createElement('div');
+ showMoreToggleDiv.className = 'col-md-3 mb-2';
+ const showMoreCheckbox = document.createElement('div');
+ showMoreCheckbox.className = 'custom-control custom-switch';
+ const checkId = 'hr_sm_' + Math.random().toString(36).substring(7);
+ this.showMoreCheck = document.createElement('input');
+ this.showMoreCheck.type = 'checkbox';
+ this.showMoreCheck.className = 'custom-control-input';
+ this.showMoreCheck.id = checkId;
+ this.showMoreCheck.checked = !!this.data.showMoreBtn;
+
+ const checkLabel = document.createElement('label');
+ checkLabel.className = 'custom-control-label small font-weight-bold text-secondary';
+ checkLabel.htmlFor = checkId;
+ checkLabel.textContent = 'Hiển thị nút "Xem thêm"';
+
+ showMoreCheckbox.appendChild(this.showMoreCheck);
+ showMoreCheckbox.appendChild(checkLabel);
+ showMoreToggleDiv.appendChild(showMoreCheckbox);
+
+ const showMoreTextDiv = document.createElement('div');
+ showMoreTextDiv.className = 'col-md-4 mb-2';
+ showMoreTextDiv.innerHTML = '';
+ this.showMoreTextInput = document.createElement('input');
+ this.showMoreTextInput.type = 'text';
+ this.showMoreTextInput.className = 'form-control form-control-sm';
+ this.showMoreTextInput.value = this.data.showMoreText;
+ showMoreTextDiv.appendChild(this.showMoreTextInput);
+
+ const showMoreUrlDiv = document.createElement('div');
+ showMoreUrlDiv.className = 'col-md-5 mb-2';
+ showMoreUrlDiv.innerHTML = '';
+ this.showMoreUrlInput = document.createElement('input');
+ this.showMoreUrlInput.type = 'text';
+ this.showMoreUrlInput.className = 'form-control form-control-sm';
+ this.showMoreUrlInput.value = this.data.showMoreUrl;
+ showMoreUrlDiv.appendChild(this.showMoreUrlInput);
+
+ moreRow.appendChild(showMoreToggleDiv);
+ moreRow.appendChild(showMoreTextDiv);
+ moreRow.appendChild(showMoreUrlDiv);
+ this.wrapper.appendChild(moreRow);
+
+ // Items Container
+ this.itemsContainer = document.createElement('div');
+ this.itemsContainer.className = 'hr-items-list mb-3';
+ this.wrapper.appendChild(this.itemsContainer);
+
+ this.data.items.forEach((item, index) => {
+ this._renderItemRow(item, index);
+ });
+
+ // Add New Item Button
+ const addBtn = document.createElement('button');
+ addBtn.type = 'button';
+ addBtn.className = 'btn btn-sm btn-outline-primary font-weight-bold mt-2';
+ addBtn.innerHTML = ' Thêm Bác sĩ / Nhân sự';
+ addBtn.addEventListener('click', (e) => {
+ e.preventDefault();
+ const newItem = {
+ degree: 'BS.',
+ name: 'Họ và Tên Bác sĩ',
+ position: 'Chức danh / Chuyên khoa',
+ avatarUrl: '',
+ specialtyIconUrl: '',
+ profileUrl: '#',
+ profileBtnText: 'Xem hồ sơ',
+ bookingUrl: '#',
+ bookingBtnText: 'Đặt lịch khám',
+ active: true
+ };
+ this.data.items.push(newItem);
+ this._renderItemRow(newItem, this.data.items.length - 1);
+ });
+
+ this.wrapper.appendChild(addBtn);
+
+ return this.wrapper;
+ }
+
+ _openPicker(currentUrl, onSelect) {
+ if (window.SISMediaPicker && typeof window.SISMediaPicker.open === 'function') {
+ window.SISMediaPicker.open(function (url) {
+ if (url) onSelect(url);
+ }, currentUrl);
+ } else {
+ const url = prompt('Nhập URL hình ảnh:', currentUrl || '');
+ if (url !== null) onSelect(url.trim());
+ }
+ }
+
+ _renderItemRow(item, index) {
+ const self = this;
+ const card = document.createElement('div');
+ card.className = 'card mb-3 shadow-sm border hr-item-card';
+
+ const cardHeader = document.createElement('div');
+ cardHeader.className = 'card-header bg-white py-2 px-3 d-flex align-items-center justify-content-between';
+ cardHeader.innerHTML = `
+ #${index + 1}: ${item.degree ? item.degree + ' ' : ''}${item.name || 'Bác sĩ'}
+
+
+
+
+
+ `;
+
+ const cardBody = document.createElement('div');
+ cardBody.className = 'card-body p-3';
+
+ // Row 1: Degree, Name, Position
+ const row1 = document.createElement('div');
+ row1.className = 'form-row mb-2';
+
+ const degreeCol = document.createElement('div'); degreeCol.className = 'col-md-2 mb-2';
+ degreeCol.innerHTML = '';
+ const degreeInput = document.createElement('input'); degreeInput.type = 'text'; degreeInput.className = 'form-control form-control-sm';
+ degreeInput.value = item.degree || '';
+ degreeInput.placeholder = 'BS CKII., TS BS...';
+ degreeInput.addEventListener('input', () => { item.degree = degreeInput.value; });
+ degreeCol.appendChild(degreeInput);
+
+ const nameCol = document.createElement('div'); nameCol.className = 'col-md-5 mb-2';
+ nameCol.innerHTML = '';
+ const nameInput = document.createElement('input'); nameInput.type = 'text'; nameInput.className = 'form-control form-control-sm';
+ nameInput.value = item.name || '';
+ nameInput.placeholder = 'Nguyễn Viết Hậu...';
+ nameInput.addEventListener('input', () => {
+ item.name = nameInput.value;
+ const headerText = cardHeader.querySelector('.font-weight-bold');
+ if (headerText) headerText.textContent = `#${index + 1}: ${item.degree ? item.degree + ' ' : ''}${item.name}`;
+ });
+ nameCol.appendChild(nameInput);
+
+ const posCol = document.createElement('div'); posCol.className = 'col-md-5 mb-2';
+ posCol.innerHTML = '';
+ const posInput = document.createElement('input'); posInput.type = 'text'; posInput.className = 'form-control form-control-sm';
+ posInput.value = item.position || '';
+ posInput.placeholder = 'Trưởng Khoa Cấp cứu...';
+ posInput.addEventListener('input', () => { item.position = posInput.value; });
+ posCol.appendChild(posInput);
+
+ row1.appendChild(degreeCol); row1.appendChild(nameCol); row1.appendChild(posCol);
+ cardBody.appendChild(row1);
+
+ // Row 2: Avatar Image (with SISMediaPicker), Specialty Icon (with SISMediaPicker)
+ const row2 = document.createElement('div');
+ row2.className = 'form-row mb-2';
+
+ // Avatar
+ const avatarCol = document.createElement('div'); avatarCol.className = 'col-md-6 mb-2';
+ avatarCol.innerHTML = '';
+ const avatarGroup = document.createElement('div'); avatarGroup.className = 'input-group input-group-sm';
+ const avatarInput = document.createElement('input'); avatarInput.type = 'text'; avatarInput.className = 'form-control';
+ avatarInput.value = item.avatarUrl || '';
+ avatarInput.placeholder = '/uploads/... hoặc URL ảnh';
+ avatarInput.addEventListener('input', () => { item.avatarUrl = avatarInput.value.trim(); });
+
+ const avatarAppend = document.createElement('div'); avatarAppend.className = 'input-group-append';
+ const avatarPickBtn = document.createElement('button'); avatarPickBtn.type = 'button'; avatarPickBtn.className = 'btn btn-outline-secondary';
+ avatarPickBtn.innerHTML = ' Thư viện';
+ avatarPickBtn.addEventListener('click', (e) => {
+ e.preventDefault();
+ self._openPicker(item.avatarUrl, (selectedUrl) => {
+ item.avatarUrl = selectedUrl;
+ avatarInput.value = selectedUrl;
+ });
+ });
+ avatarAppend.appendChild(avatarPickBtn);
+ avatarGroup.appendChild(avatarInput); avatarGroup.appendChild(avatarAppend);
+ avatarCol.appendChild(avatarGroup);
+
+ // Specialty Icon
+ const iconCol = document.createElement('div'); iconCol.className = 'col-md-6 mb-2';
+ iconCol.innerHTML = '';
+ const iconGroup = document.createElement('div'); iconGroup.className = 'input-group input-group-sm';
+ const iconInput = document.createElement('input'); iconInput.type = 'text'; iconInput.className = 'form-control';
+ iconInput.value = item.specialtyIconUrl || '';
+ iconInput.placeholder = '/uploads/icon.png hoặc URL';
+ iconInput.addEventListener('input', () => { item.specialtyIconUrl = iconInput.value.trim(); });
+
+ const iconAppend = document.createElement('div'); iconAppend.className = 'input-group-append';
+ const iconPickBtn = document.createElement('button'); iconPickBtn.type = 'button'; iconPickBtn.className = 'btn btn-outline-secondary';
+ iconPickBtn.innerHTML = ' Thư viện';
+ iconPickBtn.addEventListener('click', (e) => {
+ e.preventDefault();
+ self._openPicker(item.specialtyIconUrl, (selectedUrl) => {
+ item.specialtyIconUrl = selectedUrl;
+ iconInput.value = selectedUrl;
+ });
+ });
+ iconAppend.appendChild(iconPickBtn);
+ iconGroup.appendChild(iconInput); iconGroup.appendChild(iconAppend);
+ iconCol.appendChild(iconGroup);
+
+ row2.appendChild(avatarCol); row2.appendChild(iconCol);
+ cardBody.appendChild(row2);
+
+ // Row 3: Action Buttons (Profile URL, Profile Btn Text, Booking URL, Booking Btn Text)
+ const row3 = document.createElement('div');
+ row3.className = 'form-row mb-2';
+
+ const profUrlCol = document.createElement('div'); profUrlCol.className = 'col-md-3 mb-2';
+ profUrlCol.innerHTML = '';
+ const profUrlInput = document.createElement('input'); profUrlInput.type = 'text'; profUrlInput.className = 'form-control form-control-sm';
+ profUrlInput.value = item.profileUrl || '#';
+ profUrlInput.addEventListener('input', () => { item.profileUrl = profUrlInput.value.trim(); });
+ profUrlCol.appendChild(profUrlInput);
+
+ const profTextCol = document.createElement('div'); profTextCol.className = 'col-md-3 mb-2';
+ profTextCol.innerHTML = '';
+ const profTextInput = document.createElement('input'); profTextInput.type = 'text'; profTextInput.className = 'form-control form-control-sm';
+ profTextInput.value = item.profileBtnText || 'Xem hồ sơ';
+ profTextInput.addEventListener('input', () => { item.profileBtnText = profTextInput.value; });
+ profTextCol.appendChild(profTextInput);
+
+ const bookUrlCol = document.createElement('div'); bookUrlCol.className = 'col-md-3 mb-2';
+ bookUrlCol.innerHTML = '';
+ const bookUrlInput = document.createElement('input'); bookUrlInput.type = 'text'; bookUrlInput.className = 'form-control form-control-sm';
+ bookUrlInput.value = item.bookingUrl || '#';
+ bookUrlInput.addEventListener('input', () => { item.bookingUrl = bookUrlInput.value.trim(); });
+ bookUrlCol.appendChild(bookUrlInput);
+
+ const bookTextCol = document.createElement('div'); bookTextCol.className = 'col-md-3 mb-2';
+ bookTextCol.innerHTML = '';
+ const bookTextInput = document.createElement('input'); bookTextInput.type = 'text'; bookTextInput.className = 'form-control form-control-sm';
+ bookTextInput.value = item.bookingBtnText || 'Đặt lịch khám';
+ bookTextInput.addEventListener('input', () => { item.bookingBtnText = bookTextInput.value; });
+ bookTextCol.appendChild(bookTextInput);
+
+ row3.appendChild(profUrlCol); row3.appendChild(profTextCol);
+ row3.appendChild(bookUrlCol); row3.appendChild(bookTextCol);
+ cardBody.appendChild(row3);
+
+ card.appendChild(cardHeader);
+ card.appendChild(cardBody);
+
+ // Event handlers for move/delete
+ cardHeader.querySelector('.move-up-btn').addEventListener('click', (e) => {
+ e.preventDefault();
+ if (index > 0) {
+ const temp = this.data.items[index];
+ this.data.items[index] = this.data.items[index - 1];
+ this.data.items[index - 1] = temp;
+ this._refreshItemsUI();
+ }
+ });
+
+ cardHeader.querySelector('.move-down-btn').addEventListener('click', (e) => {
+ e.preventDefault();
+ if (index < this.data.items.length - 1) {
+ const temp = this.data.items[index];
+ this.data.items[index] = this.data.items[index + 1];
+ this.data.items[index + 1] = temp;
+ this._refreshItemsUI();
+ }
+ });
+
+ cardHeader.querySelector('.delete-btn').addEventListener('click', (e) => {
+ e.preventDefault();
+ if (confirm('Bạn có chắc muốn xóa bác sĩ này?')) {
+ this.data.items.splice(index, 1);
+ this._refreshItemsUI();
+ }
+ });
+
+ this.itemsContainer.appendChild(card);
+ }
+
+ _refreshItemsUI() {
+ this.itemsContainer.innerHTML = '';
+ this.data.items.forEach((item, idx) => {
+ this._renderItemRow(item, idx);
+ });
+ }
+
+ save() {
+ return {
+ title: this.titleInput ? this.titleInput.value.trim() : (this.data.title || 'Đội ngũ nhân sự'),
+ cols: this.colsSelect ? parseInt(this.colsSelect.value) : (this.data.cols || 3),
+ globalClass: this.globalClassInput ? this.globalClassInput.value.trim() : (this.data.globalClass || ''),
+ globalId: this.globalIdInput ? this.globalIdInput.value.trim() : (this.data.globalId || ''),
+ globalAttributes: this.data.globalAttributes || '',
+ showMoreBtn: this.showMoreCheck ? this.showMoreCheck.checked : !!this.data.showMoreBtn,
+ showMoreText: this.showMoreTextInput ? this.showMoreTextInput.value.trim() : (this.data.showMoreText || 'Xem thêm'),
+ showMoreUrl: this.showMoreUrlInput ? this.showMoreUrlInput.value.trim() : (this.data.showMoreUrl || '#'),
+ items: this.data.items || []
+ };
+ }
+ }
+
+ // Register the plugin globally
+ window.SISEditorPlugins = window.SISEditorPlugins || {};
+ window.SISEditorPlugins['human_resource'] = { class: SISHumanResourceTool };
+ window.SISEditorPlugins['humanResource'] = { class: SISHumanResourceTool };
+ window.SISEditorPlugins['human-resource'] = { class: SISHumanResourceTool };
+})();
diff --git a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/tiny-mce.js b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/tiny-mce.js
index 47be383e..06f2e823 100644
--- a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/tiny-mce.js
+++ b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/tiny-mce.js
@@ -194,7 +194,7 @@
},
image_dimensions: true,
object_resizing: 'img',
- content_style: 'body { font-family: Roboto, ui-sans-serif, sans-serif; font-size: 14px; color: #333; } img { max-width: 100%; height: auto; cursor: pointer; display: inline-block; }',
+ content_style: 'body { font-family: Roboto, ui-sans-serif, sans-serif; font-size: 14px; color: #333; } img { max-width: 100%; height: auto; cursor: pointer; display: inline-block; } p[style*="text-align: center"] img, p[style*="text-align:center"] img, div[style*="text-align: center"] img, div[style*="text-align:center"] img, figure[style*="text-align: center"] img, figure[style*="text-align:center"] img, img[style*="text-align: center"], img[style*="text-align:center"], img.aligncenter, figure.aligncenter img { display: block !important; margin-left: auto !important; margin-right: auto !important; }',
paste_postprocess: function(plugin, args) {
if (args && args.node) {
args.node.querySelectorAll('img').forEach(function(img) {
diff --git a/sisvietnamvn_main/src/main/resources/templates/manage/menus/edit.html b/sisvietnamvn_main/src/main/resources/templates/manage/menus/edit.html
index 85fa0c26..23e0a351 100644
--- a/sisvietnamvn_main/src/main/resources/templates/manage/menus/edit.html
+++ b/sisvietnamvn_main/src/main/resources/templates/manage/menus/edit.html
@@ -66,7 +66,11 @@
-
+
+
+
+
+
@@ -79,6 +83,9 @@
+
+
+
@@ -114,6 +121,7 @@
th:data-label="${topItem.label}"
th:data-url="${topItem.url}"
th:data-title="${topItem.title}"
+ th:data-description="${topItem.description}"
th:data-image-url="${topItem.imageUrl}"
th:data-parent-id="${topItem.parent != null ? topItem.parent.id : ''}"
th:data-display-order="${topItem.displayOrder}">Edit
@@ -139,6 +147,7 @@
th:data-label="${child.label}"
th:data-url="${child.url}"
th:data-title="${child.title}"
+ th:data-description="${child.description}"
th:data-image-url="${child.imageUrl}"
th:data-parent-id="${child.parent != null ? child.parent.id : ''}"
th:data-display-order="${child.displayOrder}">Edit
@@ -163,6 +172,7 @@
th:data-label="${grandchild.label}"
th:data-url="${grandchild.url}"
th:data-title="${grandchild.title}"
+ th:data-description="${grandchild.description}"
th:data-image-url="${grandchild.imageUrl}"
th:data-parent-id="${grandchild.parent != null ? grandchild.parent.id : ''}"
th:data-display-order="${grandchild.displayOrder}">Edit
@@ -170,6 +180,32 @@
th:data-action="@{/manage/menus/{menuId}/items/{itemId}/delete(menuId=${menu.id}, itemId=${grandchild.id})}">Delete
+
+
+
+
+ Link Text
+ Tier 4
+
+ URL
+
+
+ 0
+
+
+
+
+
@@ -208,7 +244,11 @@