feat: implement dynamic training course tab system and modular PDF data migration scripts

This commit is contained in:
2026-07-27 17:00:43 +07:00
parent 54e420499f
commit 1a8726285c
98 changed files with 8041 additions and 3949 deletions
@@ -1 +1 @@
/* PLEASE DO NOT COPY AND PASTE THIS CODE. */(function(){var w=window,C='___grecaptcha_cfg',cfg=w[C]=w[C]||{},N='grecaptcha';var gr=w[N]=w[N]||{};gr.ready=gr.ready||function(f){(cfg['fns']=cfg['fns']||[]).push(f);};w['__recaptcha_api']='https://www.google.com/recaptcha/api2/';(cfg['render']=cfg['render']||[]).push('onload');(cfg['anchor-ms']=cfg['anchor-ms']||[]).push(20000);(cfg['execute-ms']=cfg['execute-ms']||[]).push(30000);w['__google_recaptcha_client']=true;var d=document,po=d.createElement('script');po.type='text/javascript';po.async=true; po.charset='utf-8';var v=w.navigator,m=d.createElement('meta');m.httpEquiv='origin-trial';m.content='A7vZI3v+Gz7JfuRolKNM4Aff6zaGuT7X0mf3wtoZTnKv6497cVMnhy03KDqX7kBz/q/iidW7srW31oQbBt4VhgoAAACUeyJvcmlnaW4iOiJodHRwczovL3d3dy5nb29nbGUuY29tOjQ0MyIsImZlYXR1cmUiOiJEaXNhYmxlVGhpcmRQYXJ0eVN0b3JhZ2VQYXJ0aXRpb25pbmczIiwiZXhwaXJ5IjoxNzU3OTgwODAwLCJpc1N1YmRvbWFpbiI6dHJ1ZSwiaXNUaGlyZFBhcnR5Ijp0cnVlfQ==';if(v&&v.cookieDeprecationLabel){v.cookieDeprecationLabel.getValue().then(function(l){if(l!=='treatment_1.1'&&l!=='treatment_1.2'&&l!=='control_1.1'){d.head.prepend(m);}});}else{d.head.prepend(m);}po.src='https://www.gstatic.com/recaptcha/releases/A7KpaEASfhDcK0nXxgQEyyYv/recaptcha__en.js';po.crossOrigin='anonymous';po.integrity='sha384-DMJucfgjcmtc4a8x9gFfPgwoWXK+1qozk7K/wTFGVtduYs3wg2BI8Z5lrJXZV+iE';var e=d.querySelector('script[nonce]'),n=e&&(e['nonce']||e.getAttribute('nonce'));if(n){po.setAttribute('nonce',n);}var s=d.getElementsByTagName('script')[0];s.parentNode.insertBefore(po, s);})();
/* PLEASE DO NOT COPY AND PASTE THIS CODE. */(function(){var w=window,C='___grecaptcha_cfg',cfg=w[C]=w[C]||{},N='grecaptcha';var gr=w[N]=w[N]||{};gr.ready=gr.ready||function(f){(cfg['fns']=cfg['fns']||[]).push(f);};w['__recaptcha_api']='https://www.google.com/recaptcha/api2/';(cfg['render']=cfg['render']||[]).push('onload');(cfg['anchor-ms']=cfg['anchor-ms']||[]).push(20000);(cfg['execute-ms']=cfg['execute-ms']||[]).push(30000);w['__google_recaptcha_client']=true;var d=document,po=d.createElement('script');po.type='text/javascript';po.async=true; po.charset='utf-8';po.src='https://www.gstatic.com/recaptcha/releases/A7KpaEASfhDcK0nXxgQEyyYv/recaptcha__en.js';po.crossOrigin='anonymous';po.integrity='sha384-DMJucfgjcmtc4a8x9gFfPgwoWXK+1qozk7K/wTFGVtduYs3wg2BI8Z5lrJXZV+iE';var e=d.querySelector('script[nonce]'),n=e&&(e['nonce']||e.getAttribute('nonce'));if(n){po.setAttribute('nonce',n);}var s=d.getElementsByTagName('script')[0];s.parentNode.insertBefore(po, s);})();
File diff suppressed because one or more lines are too long
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- This is a WordPress eXtended RSS file generated by WordPress as an export of your site. -->
<!-- It contains information about your site's posts, pages, comments, categories, and other content. -->
<!-- You may use this file to transfer that content from one site to another. -->
<!-- This file is not intended to serve as a complete backup of your site. -->
<!-- To import this information into a WordPress site follow these steps: -->
<!-- 1. Log in to that site as an administrator. -->
<!-- 2. Go to Tools: Import in the WordPress admin panel. -->
<!-- 3. Install the "WordPress" importer from the list. -->
<!-- 4. Activate & Run Importer. -->
<!-- 5. Upload this file using the form provided on that page. -->
<!-- 6. You will first be asked to map the authors in this export file to users -->
<!-- on the site. For each author, you may choose to map to an -->
<!-- existing user on the site or to create a new user. -->
<!-- 7. WordPress will then import each of the posts, pages, comments, categories, etc. -->
<!-- contained in this file into your site. -->
<!-- generator="WordPress/5.7.15" created="2026-07-25 11:30" -->
<rss version="2.0"
xmlns:excerpt="http://wordpress.org/export/1.2/excerpt/"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:wfw="http://wellformedweb.org/CommentAPI/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:wp="http://wordpress.org/export/1.2/"
>
<channel>
<title>Bệnh Viện Đa Khoa Quốc Tế S.I.S Cần Thơ</title>
<link>https://sisvietnam.vn</link>
<description>Cứu tinh cho bệnh nhân đột quỵ</description>
<pubDate>Sat, 25 Jul 2026 11:30:44 +0000</pubDate>
<language>vi</language>
<wp:wxr_version>1.2</wp:wxr_version>
<wp:base_site_url>https://sisvietnam.vn</wp:base_site_url>
<wp:base_blog_url>https://sisvietnam.vn</wp:base_blog_url>
<wp:author><wp:author_id>7</wp:author_id><wp:author_login><![CDATA[ducthinh]]></wp:author_login><wp:author_email><![CDATA[ducthinh@dotquy.vn]]></wp:author_email><wp:author_display_name><![CDATA[Nguyen Duc Thinh]]></wp:author_display_name><wp:author_first_name><![CDATA[Nguyễn]]></wp:author_first_name><wp:author_last_name><![CDATA[Đức Thịnh]]></wp:author_last_name></wp:author>
<wp:author><wp:author_id>9</wp:author_id><wp:author_login><![CDATA[cuonghk]]></wp:author_login><wp:author_email><![CDATA[cuonghk@dotquy.vn]]></wp:author_email><wp:author_display_name><![CDATA[Hồ Kim Cương]]></wp:author_display_name><wp:author_first_name><![CDATA[Hồ Kim]]></wp:author_first_name><wp:author_last_name><![CDATA[Cương]]></wp:author_last_name></wp:author>
<wp:author><wp:author_id>10</wp:author_id><wp:author_login><![CDATA[chiennv]]></wp:author_login><wp:author_email><![CDATA[chiennv@dotquy.vn]]></wp:author_email><wp:author_display_name><![CDATA[Nguyen Chien]]></wp:author_display_name><wp:author_first_name><![CDATA[Chien]]></wp:author_first_name><wp:author_last_name><![CDATA[Nguyen]]></wp:author_last_name></wp:author>
<wp:author><wp:author_id>12</wp:author_id><wp:author_login><![CDATA[hr]]></wp:author_login><wp:author_email><![CDATA[hr@dotquy.vn]]></wp:author_email><wp:author_display_name><![CDATA[SIS HR]]></wp:author_display_name><wp:author_first_name><![CDATA[HR]]></wp:author_first_name><wp:author_last_name><![CDATA[SIS]]></wp:author_last_name></wp:author>
<wp:author><wp:author_id>13</wp:author_id><wp:author_login><![CDATA[nlytrang]]></wp:author_login><wp:author_email><![CDATA[trangnly@dotquy.vn]]></wp:author_email><wp:author_display_name><![CDATA[Nguyễn Lê Y Trang]]></wp:author_display_name><wp:author_first_name><![CDATA[Trang]]></wp:author_first_name><wp:author_last_name><![CDATA[Nguyễn Lê Y]]></wp:author_last_name></wp:author>
<wp:author><wp:author_id>14</wp:author_id><wp:author_login><![CDATA[huynhhuy]]></wp:author_login><wp:author_email><![CDATA[huyh@dotquy.vn]]></wp:author_email><wp:author_display_name><![CDATA[Huỳnh Huy]]></wp:author_display_name><wp:author_first_name><![CDATA[Huy]]></wp:author_first_name><wp:author_last_name><![CDATA[Huỳnh]]></wp:author_last_name></wp:author>
<wp:author><wp:author_id>16</wp:author_id><wp:author_login><![CDATA[hienpvd]]></wp:author_login><wp:author_email><![CDATA[hienphanfromvietnam@gmail.com]]></wp:author_email><wp:author_display_name><![CDATA[Phan Võ Dinh Hiển]]></wp:author_display_name><wp:author_first_name><![CDATA[Hiển]]></wp:author_first_name><wp:author_last_name><![CDATA[Phan Võ Dinh]]></wp:author_last_name></wp:author>
<wp:author><wp:author_id>18</wp:author_id><wp:author_login><![CDATA[tlhanh]]></wp:author_login><wp:author_email><![CDATA[anhtlh@dotquy.vn]]></wp:author_email><wp:author_display_name><![CDATA[Trương Lâm Huỳnh Anh]]></wp:author_display_name><wp:author_first_name><![CDATA[Anh]]></wp:author_first_name><wp:author_last_name><![CDATA[Trương Lâm Huỳnh]]></wp:author_last_name></wp:author>
<wp:category>
<wp:term_id>204</wp:term_id>
<wp:category_nicename><![CDATA[dao-tao]]></wp:category_nicename>
<wp:category_parent><![CDATA[]]></wp:category_parent>
<wp:cat_name><![CDATA[Đào tạo]]></wp:cat_name>
<wp:termmeta>
<wp:meta_key><![CDATA[page_banner]]></wp:meta_key>
<wp:meta_value><![CDATA[]]></wp:meta_value>
</wp:termmeta>
<wp:termmeta>
<wp:meta_key><![CDATA[_page_banner]]></wp:meta_key>
<wp:meta_value><![CDATA[field_5bb8f3adc5d94]]></wp:meta_value>
</wp:termmeta>
</wp:category>
<generator>https://wordpress.org/?v=5.7.15</generator>
<image>
<url>https://sisvietnam.vn/wp-content/uploads/2018/08/cropped-favicon-1-32x32.png</url>
<title>Bệnh Viện Đa Khoa Quốc Tế S.I.S Cần Thơ</title>
<link>https://sisvietnam.vn</link>
<width>32</width>
<height>32</height>
</image>
</channel>
</rss>
+52
View File
@@ -0,0 +1,52 @@
import os
templates_to_update = [
'src/main/resources/templates/dao-tao-detail.html',
'src/main/resources/templates/posts/full-width.html',
'src/main/resources/templates/posts/livestream.html',
'src/main/resources/templates/posts/sidebar.html',
'src/main/resources/templates/posts/standard.html'
]
switch_script = """
<script>
document.addEventListener('click', function(e) {
if (e.target.classList.contains('custom-tab-btn')) {
var btn = e.target;
var container = btn.closest('.custom-tabs-wrapper');
if (!container) return;
var idx = btn.getAttribute('data-tab-idx');
container.querySelectorAll('.custom-tab-btn').forEach(function(b) {
b.classList.remove('active');
b.style.background = '#f8f9fa';
b.style.color = 'black';
});
btn.classList.add('active');
btn.style.background = '#0055a5';
btn.style.color = 'white';
container.querySelectorAll('.custom-tab-panel').forEach(function(p) {
p.style.display = 'none';
});
var activePanel = container.querySelector('.custom-tab-panel[data-tab-idx="' + idx + '"]');
if (activePanel) activePanel.style.display = 'block';
}
});
</script>
"""
for filepath in templates_to_update:
if not os.path.exists(filepath):
continue
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
if "document.addEventListener('click', function(e) {" not in content:
content = content.replace("</body>", switch_script + "</body>")
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Added script to {filepath}")
else:
print(f"Script already in {filepath}")
+23
View File
@@ -0,0 +1,23 @@
with open('src/main/resources/static/css/custom.css', 'a') as f:
f.write('''
/* CSS cho Tab Đa năng */
.custom-tab-btn {
padding: 10px 20px;
border: 1px solid #ccc;
background-color: #f8f9fa;
color: black;
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.custom-tab-btn:hover {
background-color: #e9ecef;
}
.custom-tab-btn.active {
background-color: #0055a5;
color: white;
border-color: #0055a5;
}
''')
+32
View File
@@ -0,0 +1,32 @@
path = '/home/x79/sisvietnamvn_01/sisvietnamvn_main/src/main/resources/templates/dao-tao-detail.html'
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
def clean_style(text):
start = 0
while True:
idx = text.find(' style="', start)
if idx == -1:
idx = text.find(" style='", start)
if idx == -1:
break
quote = text[idx + 7]
end_idx = text.find(quote, idx + 8)
if end_idx == -1:
break
style_content = text[idx+8:end_idx]
if "animation-composition" in style_content:
text = text[:idx] + text[end_idx+1:]
else:
start = end_idx + 1
return text
new_content = clean_style(content)
with open(path, 'w', encoding='utf-8') as f:
f.write(new_content)
print("Cleaned HTML!")
+74
View File
@@ -0,0 +1,74 @@
import re
import os
html_path = '/home/x79/sisvietnamvn_01/sisvietnamvn_main/src/main/resources/templates/dao-tao-detail.html'
css_path = '/home/x79/sisvietnamvn_01/sisvietnamvn_main/src/main/resources/static/css/custom.css'
with open(html_path, 'r', encoding='utf-8') as f:
content = f.read()
css_rules = []
class_counter = 1
def process_html(text):
global class_counter
start = 0
while True:
idx = text.find(' style="', start)
if idx == -1:
idx = text.find(" style='", start)
if idx == -1:
break
quote = text[idx + 7]
end_idx = text.find(quote, idx + 8)
if end_idx == -1:
break
style_content = text[idx+8:end_idx]
if "animation-composition" in style_content:
new_class = f"devtools-style-{class_counter}"
class_counter += 1
# Format CSS rule
styles = [s.strip() for s in style_content.split(';') if s.strip()]
css_rule = f".{new_class} {{\n"
for s in styles:
css_rule += f" {s};\n"
css_rule += "}\n"
css_rules.append(css_rule)
# Remove style attribute
text = text[:idx] + text[end_idx+1:]
# Now we need to inject new_class into the nearest preceding class="..."
# Search backwards for `class="` before `idx` (but only within the current tag, so stop at `<`)
tag_start = text.rfind('<', 0, idx)
class_idx = text.find('class="', tag_start, idx)
if class_idx != -1:
# insert new_class inside the existing class attribute
insert_pos = class_idx + 7
text = text[:insert_pos] + new_class + " " + text[insert_pos:]
start = idx # adjust start roughly
else:
# element has no class attribute, insert one where style was
text = text[:idx] + f' class="{new_class}"' + text[idx:]
start = idx
else:
start = end_idx + 1
return text
new_html = process_html(content)
if css_rules:
with open(css_path, 'a', encoding='utf-8') as f:
f.write("\n/* --- Extracted Inline Styles --- */\n")
f.write("\n".join(css_rules))
with open(html_path, 'w', encoding='utf-8') as f:
f.write(new_html)
print(f"Extracted {len(css_rules)} styles to custom.css")
else:
print("No styles found.")
+15
View File
@@ -0,0 +1,15 @@
import pdfplumber
import re
file_path = '/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/2.-CHUONG-TRINH-CAN-THIEP-MACH-MAU-THAN-KINH-NANG-CAO-BO-SUNG-FINAL.pdf'
with pdfplumber.open(file_path) as pdf:
text = ''
for page in pdf.pages:
text += page.extract_text() + '\n'
lines = text.split('\n')
for line in lines:
line_lower = line.lower()
if any(k in line_lower for k in ['thời gian', 'đối tượng', 'thông tin lớp học', 'số lượng', 'hồ sơ', 'học phí', 'địa điểm nhận', 'hình thức']):
print(line.strip())
+107
View File
@@ -0,0 +1,107 @@
import os
templates_to_update = [
'src/main/resources/templates/dao-tao-detail.html',
'src/main/resources/templates/posts/full-width.html',
'src/main/resources/templates/posts/livestream.html',
'src/main/resources/templates/posts/sidebar.html',
'src/main/resources/templates/posts/standard.html'
]
switch_script = """
<script>
document.addEventListener('click', function(e) {
if (e.target.classList.contains('custom-tab-btn')) {
var btn = e.target;
var container = btn.closest('.custom-tabs-wrapper');
if (!container) return;
var idx = btn.getAttribute('data-tab-idx');
container.querySelectorAll('.custom-tab-btn').forEach(function(b) {
b.classList.remove('active');
b.style.background = '#f8f9fa';
b.style.color = 'black';
});
btn.classList.add('active');
btn.style.background = '#0055a5';
btn.style.color = 'white';
container.querySelectorAll('.custom-tab-panel').forEach(function(p) {
p.style.display = 'none';
});
var activePanel = container.querySelector('.custom-tab-panel[data-tab-idx="' + idx + '"]');
if (activePanel) activePanel.style.display = 'block';
}
});
</script>
"""
for filepath in templates_to_update:
if not os.path.exists(filepath):
continue
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# First, remove the script from wherever it is (at the bottom)
if "document.addEventListener('click', function(e) {" in content:
# Find the script block
start_idx = content.find("document.addEventListener('click', function(e) {")
script_start = content.rfind("<script>", 0, start_idx)
script_end = content.find("</script>", start_idx) + 9
# Remove it
content = content[:script_start].rstrip() + "\n" + content[script_end:].lstrip()
# Now find where layout:fragment="content" ends
# We can just look for the last closing </div> inside the document before </body>
# Wait, the easiest way is to inject it right after the Editor parsing script which is INSIDE the fragment!
# The editor parsing script ends with `});\n </script>\n </div>` in standard.html.
# Actually, in standard.html, the script is inside `<div class="post-content ..."> ... </div>` which is inside `<div layout:fragment="content">`.
# Let's just find the last `</script>` that is inside the content.
# A better way: Replace the end of the script block that parses Editor content.
# We know that script ends with `});` and `</script>`.
# In standard.html, `parseEditorContent` is not there. It uses `h.parentNode.replaceChild(p, h);`
# Let's just put our switch_script right before `</body>` if it's NOT a layout decorated file? No, all 5 are decorated!
# Wait, they are all Thymeleaf files with `layout:decorate`. So they must be put inside the `layout:fragment="content"`.
# Let's just put the switch_script right after `var html = finalHtml; }` or just at the end of the `switch_script` in the parser script!
# Yes! Why add a NEW `<script>` tag? Just append the JS code to the existing `<script>` tag!
# Let's find `parseEditorContent` or the end of the inline script.
# Let's just insert the plain JS right after `html = finalHtml;`
js_code = """
document.addEventListener('click', function(e) {
if (e.target.classList.contains('custom-tab-btn')) {
var btn = e.target;
var container = btn.closest('.custom-tabs-wrapper');
if (!container) return;
var idx = btn.getAttribute('data-tab-idx');
container.querySelectorAll('.custom-tab-btn').forEach(function(b) {
b.classList.remove('active');
b.style.background = '#f8f9fa';
b.style.color = 'black';
});
btn.classList.add('active');
btn.style.background = '#0055a5';
btn.style.color = 'white';
container.querySelectorAll('.custom-tab-panel').forEach(function(p) {
p.style.display = 'none';
});
var activePanel = container.querySelector('.custom-tab-panel[data-tab-idx="' + idx + '"]');
if (activePanel) activePanel.style.display = 'block';
}
});
"""
if "document.addEventListener('click', function(e) {" not in content:
# Wait, if we removed it, it won't be there.
# Let's just replace `html = finalHtml;` with `html = finalHtml;` + js_code
content = content.replace("html = finalHtml;", "html = finalHtml;\n" + js_code)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Fixed script position in {filepath}")
File diff suppressed because one or more lines are too long
+19
View File
@@ -0,0 +1,19 @@
import pdfplumber
import os
files = [
"/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/2.-CHUONG-TRINH-CAN-THIEP-MACH-MAU-THAN-KINH-NANG-CAO-BO-SUNG-FINAL.pdf",
"/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/CHUONG-TRINH-CAN-THIEP-MACH-MAU-CAC-TANG-VA-MACH-MAU-NGOAI-BIEN-CO-BAN.pdf",
"/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/CHUONG-TRINH-CHUAN-BI-DUNG-CU-VA-CHAM-SOC-BENH-NHAN-TRONG-PHONG-CHUP-MACH.pdf",
"/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/CHUONG-TRINH-DAO-TAO-TIM-MACH-CAN-THIEP-1.pdf"
]
for f in files:
print(f"--- {os.path.basename(f)} ---")
try:
with pdfplumber.open(f) as pdf:
page1 = pdf.pages[0].extract_text()
print(page1[:300].replace('\n', ' | '))
except Exception as e:
print(f"Error: {e}")
print()
+25
View File
@@ -0,0 +1,25 @@
import re
from bs4 import BeautifulSoup
import json
import os
with open('/home/x79/.gemini/antigravity-ide/brain/53991324-2cf9-45f5-8177-0e9acab31498/.system_generated/steps/939/content.md', 'r') as f:
html = f.read()
soup = BeautifulSoup(html, 'html.parser')
content = soup.find('div', class_='entry-content')
if not content:
print('entry-content not found.')
exit(1)
# we want to get the raw HTML string of the inner contents
inner_html = ""
for child in content.children:
inner_html += str(child)
# Let's save this raw HTML to a temp file
with open('html_snippets/content_main_blocks/temp_agenda', 'w') as f:
f.write(inner_html)
print("Saved extracted HTML to temp_agenda. Length:", len(inner_html))
+13
View File
@@ -0,0 +1,13 @@
import pdfplumber
import sys
filename = "/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/2.-CHUONG-TRINH-CAN-THIEP-MACH-MAU-THAN-KINH-NANG-CAO-BO-SUNG-FINAL.pdf"
with pdfplumber.open(filename) as pdf:
text = ""
for i, page in enumerate(pdf.pages):
text += f"--- PAGE {i+1} ---\n"
text += page.extract_text() + "\n"
with open('pdf1_output.txt', 'w', encoding='utf-8') as f:
f.write(text)
print("Saved to pdf1_output.txt")
+16
View File
@@ -0,0 +1,16 @@
import pdfplumber
import json
import re
filename = "/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/2.-CHUONG-TRINH-CAN-THIEP-MACH-MAU-THAN-KINH-NANG-CAO-BO-SUNG-FINAL.pdf"
with pdfplumber.open(filename) as pdf:
full_text = ""
for page in pdf.pages:
full_text += page.extract_text() + "\n"
print("Total length:", len(full_text))
# Find all headings
headings = re.findall(r'^(\d+)\.\s+(.*)$', full_text, re.MULTILINE)
print("Headings found:")
for h in headings:
print(h)
+7
View File
@@ -0,0 +1,7 @@
import pdfplumber
file_path = '/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/2.-CHUONG-TRINH-CAN-THIEP-MACH-MAU-THAN-KINH-NANG-CAO-BO-SUNG-FINAL.pdf'
with pdfplumber.open(file_path) as pdf:
for i, page in enumerate(pdf.pages[:3]):
print(f'--- Page {i} ---')
print(page.extract_text())
+21
View File
@@ -0,0 +1,21 @@
import pdfplumber
files = [
'/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/2.-CHUONG-TRINH-CAN-THIEP-MACH-MAU-THAN-KINH-NANG-CAO-BO-SUNG-FINAL.pdf',
'/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/CHUONG-TRINH-CAN-THIEP-MACH-MAU-CAC-TANG-VA-MACH-MAU-NGOAI-BIEN-CO-BAN.pdf',
'/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/CHUONG-TRINH-CHUAN-BI-DUNG-CU-VA-CHAM-SOC-BENH-NHAN-TRONG-PHONG-CHUP-MACH.pdf',
'/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/CHUONG-TRINH-DAO-TAO-TIM-MACH-CAN-THIEP-1.pdf'
]
for f in files:
print(f'Checking {f}')
with pdfplumber.open(f) as pdf:
text = ''
for page in pdf.pages:
text += page.extract_text() or ''
print(" Length:", len(text))
if 'Thời gian' in text or 'Đối tượng' in text or 'Học phí' in text:
print(' -> Found some keywords.')
else:
print(' -> Keywords NOT found.')
+71
View File
@@ -0,0 +1,71 @@
import pdfplumber
import re
import json
files = [
'/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/2.-CHUONG-TRINH-CAN-THIEP-MACH-MAU-THAN-KINH-NANG-CAO-BO-SUNG-FINAL.pdf',
'/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/CHUONG-TRINH-CAN-THIEP-MACH-MAU-CAC-TANG-VA-MACH-MAU-NGOAI-BIEN-CO-BAN.pdf',
'/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/CHUONG-TRINH-CHUAN-BI-DUNG-CU-VA-CHAM-SOC-BENH-NHAN-TRONG-PHONG-CHUP-MACH.pdf',
'/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/CHUONG-TRINH-DAO-TAO-TIM-MACH-CAN-THIEP-1.pdf'
]
slugs = [
"can-thiep-mach-mau-than-kinh-nang-cao",
"can-thiep-mach-mau-cac-tang-va-ngoai-bien-co-ban",
"chuan-bi-dung-cu-va-cham-soc-benh-nhan-trong-phong-chup-mach",
"tim-mach-can-thiep-co-ban"
]
def extract_section(text, start_keywords, end_keywords=None):
lines = text.split('\n')
started = False
content = []
for line in lines:
line_lower = line.lower()
if not started:
for k in start_keywords:
if k in line_lower:
started = True
# Optional: remove the keyword from the line to get just the value
# But simpler to just keep it or try to split
split_idx = line_lower.find(k) + len(k)
val = line[split_idx:].strip(': ').strip()
if val:
content.append(val)
break
else:
if end_keywords:
for k in end_keywords:
if k in line_lower:
return '\n'.join(content).strip()
# Stop if we hit a new numbered section like "10." or empty lines after some text
if re.match(r'^\d+\.', line) or re.match(r'^[A-Z\s]+$', line):
return '\n'.join(content).strip()
if line.strip():
content.append(line.strip())
return '\n'.join(content).strip()
for f, slug in zip(files, slugs):
print(f"--- Parsing {slug} ---")
with pdfplumber.open(f) as pdf:
text = ''
for page in pdf.pages:
text += page.extract_text() + '\n'
time = extract_section(text, ['thời gian đào tạo'])
audience = extract_section(text, ['đối tượng học viên', 'đối tượng tham dự', 'đối tượng'])
quantity = extract_section(text, ['số lượng học viên:', 'số lượng học viên'])
documents = extract_section(text, ['hồ sơ đăng ký tham dự', 'hồ sơ nhập học'])
tuitionFee = extract_section(text, ['học phí'])
location = extract_section(text, ['địa điểm nhận hồ sơ'])
print(f"Thời gian: {time[:50]}")
print(f"Đối tượng: {audience[:50]}")
print(f"Số lượng: {quantity[:50]}")
print(f"Hồ sơ: {documents[:50]}")
print(f"Học phí: {tuitionFee[:50]}")
print(f"Địa điểm: {location[:50]}")
print("")
+15
View File
@@ -0,0 +1,15 @@
from bs4 import BeautifulSoup
import sys
import re
with open('/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/Đào tạo tại UMC.html', 'r', encoding='utf-8') as f:
html = f.read()
soup = BeautifulSoup(html, 'html.parser')
main = soup.find('main')
if main:
with open('umc_main.html', 'w', encoding='utf-8') as f:
f.write(main.prettify())
print("Saved main tag to umc_main.html")
else:
print("No main tag found")
+28
View File
@@ -0,0 +1,28 @@
import sys
from bs4 import BeautifulSoup
import traceback
try:
with open('/home/x79/.gemini/antigravity-ide/brain/53991324-2cf9-45f5-8177-0e9acab31498/.system_generated/steps/939/content.md', 'r') as f:
html = f.read()
soup = BeautifulSoup(html, 'html.parser')
# In WordPress, the main content is often in an article tag or div with class containing 'content'
# For elementor, it's usually inside 'elementor-widget-theme-post-content'
content = soup.find('div', class_='elementor-widget-theme-post-content')
if not content:
# Let's try to find an elementor text editor div
content = soup.find('div', class_='elementor-text-editor')
if not content:
# Let's just grab the whole body
content = soup.find('body')
if content:
html_str = str(content)
with open('temp_agenda.html', 'w') as out:
out.write(html_str)
print("Success, wrote", len(html_str), "bytes to temp_agenda.html")
else:
print("Could not find any content")
except Exception as e:
traceback.print_exc()
+814
View File
@@ -0,0 +1,814 @@
--- PAGE 1 ---
SỞ Y TẾ CẦN THƠ
BỆNH VIỆN ĐA KHOA QUỐC TẾ S.I.S CẦN THƠ
CHƯƠNG TRÌNH ĐÀO TẠO
CAN THIỆP MẠCH MÁU
THẦN KINH NÂNG CAO
CẦN THƠ 2021
1
--- PAGE 2 ---
1. GIỚI THIỆU CHUNG VỀ KHÓA HỌC
Đột quỵ là nguyên nhân tử vong đứng hàng thứ ba sau bệnh lý tim mạch và ung thư.
Phần lớn đột quỵ ở dạng đột quỵ thiếu máu não (80%) và phần còn lại là đột quỵ dạng xuất
huyết (20%). Mỗi năm ở Việt Nam, có khoảng 200.000 người bị đột quỵ, trong đó 50%
bệnh nhân tử vong và 90% bệnh nhân để lại di chứng nặng nề sau đột quỵ. Cứ mỗi 40 giây
có 1 người bị đột quỵ và sau mỗi 4 phút lại có 1 bệnh nhân tử vong do đột quỵ. Theo ngân
hàng dữ liệu bệnh tật của Mỹ (CDC) thì tại Mỹ, chi phí hàng năm dành chăm sóc và điều
trị đột quỵ ước tính là 34 tỉ USD.
Với sự xuất hiện của chuyên ngành can thiệp nội mạch, đặc biệt là can thiệp mạch máu
thần kinh giúp các bác sĩ lâm sàng chẩn đoán dễ dàng các bệnh lý mạch máu não cũng như
can thiệp điều trị các tổn thương này với sự xâm lấn tối thiếu nhất nhưng mang lại hiệu quả
cao nhất. Kỹ thuật chụp mạch máu não là sử dụng hệ thống ống thông (catheter) và dây
dẫn (wire) đi từ động mạch đùi (một số trường hợp sử dụng động mạch quay hoặc động
mạch cánh tay) chọn lọc đến các động mạch não thông qua hệ thống máy chụp mạch máu
kỹ thuật số hóa xóa nền (DSA). Các kỹ thuật can thiệp mạch máu thần kinh có thể áp dụng
như: lấy huyết động mạch não cho bệnh nhân đột quỵ thiếu máu não cấp; can thiệp gây tắc
trong dị dạng động tĩnh mạch não tủy và trong rò động tĩnh mạch màng cứng não tủy;
can thiệp gây tắc trong rò động mạch cảnh xoang hang; can thiệp gây tắc phình động mạch
não (đặt coil; stent + coil; bóng + coil; stent chuyển dòng và gần đây là WEB/LUNA); gây
tắc u giàu mạch máu (u màng não; u nguyên bào mạch máu); can thiệp đặt stent ngoài sọ
và trong sọ;…
Can thiệp mạch máu thần kinh đòi hỏi các bác sĩ phải có kiến thức về bệnh học và hình
ảnh học mạch máu thần kinh sâu rộng. Một điều không thể thiếu nữa là các kỹ năng can
thiệp nhằm hạn chế biến chứng và mang lại khả năng thành công cao.
Bên cạnh chức năng quan trọng là điều trị cho bệnh nhân, Bệnh viện Đa khoa Quốc tế
S.I.S Cần Thơ còn hướng đến việc xây dựng một trung tâm nghiên cứu và đào tạo trong
lĩnh vực chẩn đoán & điều trị đột quỵ, đã hỗ trợ đào tạo cho các bác sĩ đến từ các quốc gia
đang gặp khó trong việc điều trị đột quỵ như Lào, Campuchia, Indonesia, Ấn Độ và đã
được Bộ Y tế phê duyệt trở thành trung tâm đào tạo và chuyển giao các kỹ thuật cao cho
đơn vị và bệnh viện trong vùng cũng như các nước trong khu vực với mã đào tạo là C40.08.
Trong điều kiện đó, chúng tôi xây dựng chương trình đào tạo “Can thiệp mạch máu
thần kinh nâng cao”. Với chương trình này chúng tôi hy vọng sẽ đào tạo và chuyển giao
các kỹ thuật cao cho các bác sĩ & bệnh viện trong vùng cũng như các nước trong khu vực.
Giúp cho việc chẩn đoán và can thiệp điều trị kịp thời cho bệnh nhân đột quỵ giảm
nguy cơ tàn phế vĩnh viễn và tỷ lệ tử vong.
Chương trình đào tạo “Can thiệp mạch máu thần kinh nâng cao” dành cho các bác sĩ
chẩn đoán hình ảnh; nội ngoại thần kinh đã có chứng chỉ can thiệp mạch máu thần kinh
cơ bản tại các bệnh viện trong vùng và các nước trong khu vực. Đây là chương trình đào
tạo tập trung gồm 80 tiết lý thuyết và 780 tiết thực hành kéo dài trong 6 tháng tại Bệnh
viện Đa Khoa Quốc Tế S.I.S Cần Thơ.
2
--- PAGE 3 ---
2. MỤC TIÊU CỦA KHÓA HỌC
2.1. Mục tiêu chung khóa học
Sau khi kết thúc khóa học, học viên có khả năng nhận biết, chẩn đoán, đọc kết quả và
thực hiện chụp chẩn đoán và can thiệp chuyên sâu các bệnh lý mạch máu thần kinh.
2.2. Mục tiêu cụ thể
2.2.1. Mục tiêu kiến thức
1. Chẩn đoán xác định và chẩn đoán phân biệt được tất cả các dạng bệnh lý về mạch máu
thần kinh.
2. Đánh giá và đưa ra được phương pháp điều trị tối ưu cho các bệnh nhân có các bệnh
lý mạch máu thần kinh phức tạp.
2.2.2. Mục tiêu kĩ năng
1. Đọc và phân tích được hình ảnh học mạch máu thần kinh trên MRA; CTA và DSA
bất thường dựa vào cơ sở dữ liệu của Bệnh viện và trên bệnh nhân lâm sàng.
2. Thực hiện được can thiệp lấy huyết khối và bơm thuốc tiêu sợi huyết đường động
mạch trong đột quỵ thiếu máu não cấp; nút phình và điều trị phình động mạch não bằng
thay đổi dòng chảy trong phình động mạch não; nút dị dạng thông động tĩnh mạch não và
nút dị dạng thông động tĩnh mạch màng cứng; nút dị dạng mạch tủy trong các bệnh lý
mạch máu tủy; nong và đặt stent điều trị hẹp động mạch ngoài sọ, nong hẹp động mạch nội
sọ và đặt Stent điều trị hẹp động mạch nội sọ; can thiệp các bệnh lý hệ tĩnh mạch não …
… theo quy trình kỹ thuật của bệnh viện, trên bệnh nhân tại bệnh viện
2.2.3. Mục tiêu thái độ
1. Thể hiện sử tỉ mỉ, thận trọng và quyết đoán khi đọc; phân tích hình ảnh học mạch
máu thần kinh và trong thực hiện thủ thuật chụp can thiệp mạch máu thần kinh.
2. Biểu hiện sự khẩn trương, quyết đoán khi xử lý các tình huống trong quá trình thủ thuật.
3. ĐỐI TƯỢNG HỌC VIÊN
Các bác sĩ chẩn đoán hình ảnh; nội ngoại thần kinh có đã có chứng chỉ can thiệp
mạch máu thần kinh cơ bản tại các bệnh viện trong vùng và các nước trong khu vực.
3
--- PAGE 4 ---
4. NỘI DUNG CHƯƠNG TRÌNH
Tiết
Thực hành
Mục tiêu học tập
BÀI Tổng Lí Tại
STT Tại
GIẢNG số thuyết buồng
Lab
bệnh
1. Trình bày chính xác các nguyên lý tạo hình
trên CT và MRI/ trình bày rõ ràng các cấu
trúc và các hình ảnh bình thường & bất
thường trên CT scan và MRI não.
Bài 1 2. Đọc và nhận định chính xác các mốc giải
Hình phẫu não và tủy dựa trên cơ sở dữ liệu bệnh
ảnh học viện và trên bệnh nhân.
1 30 3 7 20
CT, MRI 3. Thực hiện thuần thục các phần mềm quản
sọ não lý dữ liệu và xử lý hình ảnh nhằm truy xuất
và thu thập hình ảnh chính xác nhất.
4. Thể hiện sự tỉ mỉ, thận trọng khi quan sát
và nhận định các mốc giải phẫu não và tủy
trên CT scan & MRI.
1. Trình bày rõ ràng các cấu trúc và hình
ảnh bất thường trên CT và MRI trong chẩn
đoán nhồi máu não.
Bài 2
2. Đọc và nhận định chính xác hình ảnh bất
Hình
thường trên CT scan và MRI trong chẩn
ảnh
đoán nhồi máu não/Phân tích đầy đủ tổn
học
thương trên nhu mô và trên mạch máu của
trong
các hình ảnh học trong chẩn đoán nhồi máu
chẩn
não dựa trên cơ sở dữ liệu của bệnh viện và
2 đoán 45 3 7 35
trên bệnh nhân.
nhồi
3. Thể hiện sự tỉ mỉ và thận trọng khi đọc và
máu
phân tích hình ảnh nhồi máu não trên CT và
não
MRI.
1. Trình bày rõ ràng các cấu trúc và hình
Bài 3 ảnh bất thường trên CT và MRI trong chẩn
Hình đoán xuất huyết não.
ảnh
2. Đọc và nhận định chính xác hình ảnh bất
học
thường trên CT scan và MRI trong chẩn
trong
đoán xuất huyết não dựa trên cơ sở dữ liệu
3 chẩn 50 3 7 40
của bệnh viện và trên bệnh nhân.
đoán
3. Thể hiện được sự tỉ mỉ và thận trọng khi
xuất
4
--- PAGE 5 ---
huyết đọc và phân tích hình ảnh xuất huyết não
não trên CT và MRI.
1. Trình bày rõ ràng các cấu trúc và hình
Bài 4 ảnh bất thường trên CT và MRI trong chẩn
Hình đoán bệnh mạch máy tủy.
ảnh 2. Đọc và nhận định chính xác hình ảnh bất
học thường trên CT scan và MRI trong chẩn
trong đoán bệnh mạch máu tủy dựa trên cơ sở dữ
chẩn liệu của bệnh viện và trên bệnh nhân. 50 3 7 40
4 đoán 3. Thể hiện được sự tỉ mỉ và thận trọng khi
bệnh đọc và phân tích hình ảnh bệnh mạch máu tủy
mạch trên CT và MRI.
máu
tủy
1. Trình bày đầy đủ cách đánh giá và chẩn
đoán nhồi máu não theo đúng quy trình kĩ
thuật tại bệnh viện/ Trình bày đầy đủ 4
phương pháp điều trị nội khoa nhồi máu
não: Điều trị tổng quát & điều trị các biến
chứng cấp; kháng đông; tăng thể tích, dãn
mạch & tăng huyết áp; sử dụng tiêu sợi
Bài 5
huyết đường tĩnh mạch.
Chẩn
2. Điều trị thành thạo 4 phương pháp điều
đoán
trị nội khoa nhồi máu não tại phòng lab, 40 3 7 30
điều
khoa Hồi sức và khoa Thần kinh đột quỵ
trị nội
của bệnh viện.
khoa
5 3. Nhận thức được việp áp dụng các phương
nhồi
pháp điều trị nội khoa đóng vai trò quan
máu
trọng trong điều trị đột quỵ nhồi máu não/
não
Có thái độ thận trọng, tỉ mỉ trong đánh giá
và áp dụng các phương pháp điều trị nội
khoa.
1. Trình bày đầy đủ các chỉ định, chống chỉ
Bài 6 định của tiêu sợi huyết đường tĩnh mạch
Nhồi
máu 2. Đủ điều kiện thực hiện bơm tiêu sợi
não huyết tĩnh mạch trên bệnh nhân lâm sàng tại
cấp: khoa Cấp cứu của bệnh viện theo đúng quy
chỉ trình kỹ thuật của bệnh viện
6 định 40 3 7 30
3. Nhận thức được việc áp dụng điều trị tiêu
điều
sợi huyết quyết định hiệu quả cấp cứu đột
trị tiêu
quỵ/Có thái độ cẩn trọng, tỉ mỉ trong việc
5
--- PAGE 6 ---
sợi đánh giá và thực hiện kĩ thuật
huyết
tĩnh
mạch
1. Trình bày đầy đủ các chỉ định, chống chỉ
Bài 7 định của kĩ thuật lấy huyết khối bằng dụng
Nhồi cụ.
máu
2. Đủ điều kiện và thực hiện thành thạo 2 kỹ
não
thuật lấy huyết bằng dụng cụ trên bệnh nhân
cấp:
thật (lấy huyết khối bằng stent; hút huyết
chỉ
khối).
định
3. Đủ điều kiện và thực hiện thành thạo thực
điều
hiện các kỹ thuật can thiệp lấy huyết khối và
trị nội
7 70 3 17 50
bơm thuốc tiêu sợi huyết đường động mạch
mạch
trong đột quỵ thiếu máu não cấp; nong và
lấy
đặt stent điều trị hẹp động mạch ngoài sọ,
huyết
nong hẹp động mạch nội sọ và đặt Stent
khối
điều trị hẹp động mạch nội sọ; can thiệp các
bằng
bệnh lý hệ tĩnh mạch não.
dụng
cụ 4. Thể hiện được sự khẩn trương; quyết
đoán và tỉ mỉ khi can thiệp.
Bài 8 1. Trình bày đầy đủ vai trò & các chỉ định
Điều trong điều trị phẫu thuật mở nội sọ giải áp
trị trong nhồi máu não.
phẫu 2. Đề xuất chính xác chỉ định phẫu thuật mở
thuật sọ giải áp trong nhồi máu não khi điều trị
mở sọ cho BN trong trường hợp phải hội chẩn với
giải bác sĩ Ngoại thần kinh trên bệnh nhân
áp 3.Nhận thức được sự hiệu quả và tầm quan
8 30 3 0 27
trong trọng của việc áp dụng điều trị phẫu thuật
nhồi
mở nội sọ giải áp trong nhồi máu não.
máu
não
1. Trình bày rõ ràng cách đánh giá và chẩn
đoán điều trị nội khoa xuất huyết não-màng
não theo đúng quy trình kĩ thuật tại bệnh
Bài 9 viện/ Trình bày rõ ràng 9 biến chứng gồm
Chẩn tăng đường huyết, rối loạn điện giải, tái xuất
đoán huyết, giãn não thất, xuất huyết não, động
điều kinh, biến chứng tim mạch, phù phổi do
trị nội thần kinh, co thắt mạch não và hướng điều
9 khoa trị nội khoa trong xuất huyết não-màng não 50 3 0 47
6
--- PAGE 7 ---
xuất 2. Ứng dụng được vào thực tiễn lâm sàng
huyết chẩn đoán điều trị nội khoa xuất huyết não-
não màng não theo đúng quy trình kĩ thuật trên
màng bệnh nhân tại bệnh viện/ Đề xuất được chỉ
não định điều trị nội khoa 9 biến chứng trong
xuất huyết não-màng não
3. Thể hiện được thận trọng, tỉ mỉ & quyết
đoán trong chẩn đoán điều trị nội khoa xuất
huyết não-màng não
1. Trình bày rõ ràng vai trò & các chỉ định
Bài 10
trong điều trị phẫu thuật trong xuất huyết
Điều
não-màng não.
trị
2. 2. Đề xuất chính xác chỉ định phẫu thuật
phẫu
trong xuất huyết não-màng não/ Đánh giá
thuật
tiên lượng, kết quả và các biến chứng phẫu
trong
10 thuật xuất huyết não màng não. 30 3 0 27
xuất
huyết 3. Nhận thức được sự hiệu quả và tầm quan
não trọng của việc áp dụng điều trị phẫu thuật
màng trong xuất huyết não-màng não
não
1. Trình bày đầy đủ các chỉ định can thiệp
nội mạch và chiến lược theo dõi sau can
thiệp nội mạch trong điều trị dị dạng mạch
Bài 11
máu não.
Can
2. Trình bày rõ ràng quy trình can thiệp điều
thiệp
trị dị dạng mạch máu não theo quy định kĩ
nội
thuật tại bệnh viện.
mạch
3. Đủ điều kiện và thực hiện thành thạo các 75 5 10 60
điều
kỹ thuật can thiệp điều trị dị dạng mạch máu
trị dị
não trên bệnh nhân trong phòng can thiệp
11 dạng
DSA.
mạch
4. Thể hiện được sự quyết đoán và cẩn thận
máu
trong việc chỉ định và lựa chọn phương pháp
não
điều trị.
7
--- PAGE 8 ---
1. Trình bày đầy đủ các chỉ định và quy trình
can thiệp nội mạch điều trị phình động mạch
não theo quy định kĩ thuật tại bệnh viện và 8
phương pháp điều trị phình động mạch não
bằng can thiệp nội mạch gồm tắc động mạch
mang, gây tắc với coil đơn thuần, bóng hỗ
Bài 12
trợ coil, stent hỗ trợ coil, kỹ thuật nhiều
Can
thiệp catheter, điều trị túi phình với các dung dịch
nội
gây tắc, flow diverters, dụng cụ gây tắc túi
mạch
điều phình mới Woven Endobridge (WEB).
trị
2. Đủ điều kiện và thực hiện thành thạo các
12 phình 75 5 10 60
động kỹ thuật can thiệp phình động mạch não trên
mạch
bệnh nhân trong phòng can thiệp DSA.
não
3. Thể hiện được sự quyết đoán và cẩn thận
trong việc chỉ định và lựa chọn phương pháp
điều trị.
1. Trình bày rõ ràng các tiêu chuẩn chẩn
đoán được rò động mạch cảnh xoang hang
và các chỉ định can thiệp nội mạch trong
điều trị rò động mạch cảnh xoang hang/
Bài 13 Trình bày quy trình can thiệp điều trị rò
Chẩn động mạch cảnh xoang hang.
đoán
2. Trình bày đầy đủ 4 kỹ thuật điều trị rò
động mạch cảnh xoang hang.
điều
trị rò 3. Đủ điều kiện và thực hiện được thành
động thạo các kỹ thuật can thiệp điều trị các
13 55 5 10 40
mạch trường hợp rò động mạch cảnh xoang hang
cảnh trên bệnh nhân trong phòng can thiệp DSA.
xoang
4. Thể hiện được sự thận trọng khi can thiệp
hang
để tránh tai biến xảy ra.
8
--- PAGE 9 ---
1. Trình bày đầy đủ các tiêu chuẩn chẩn
đoán được rò động tĩnh mạch màng cứng và
các chỉ định can thiệp nội mạch trong điều
trị rò động tĩnh mạch màng cứng/ Trình bày
Bài 14
quy trình can thiệp điều trị rò động tĩnh
Chẩn
mạch màng cứng
đoán
2. Trình bày đầy đủ 5 kỹ thuật điều trị rò
điều
động tĩnh mạch màng cứng
trị rò
3. Đủ điều kiện và thực hiện được các kỹ
động
thuật can thiệp các trường hợp rò động tĩnh
14 tĩnh 55 5 10 40
mạng màng cứng trên bệnh nhân trong
mạch
phòng can thiệp DSA
màng
cứng
4. Thể hiện được sự thận trọng khi can thiệp
để tránh tai biến xảy ra.
1. Trình bày đầy đủ vai trò của điều trị phục
hồi vận động sau đột quỵ/ Trình bày cụ thể
11 chiến lược điều trị phục hồi vận động
gồm vật lý trị liệu, phòng ngừa các biến
chứng hô hấp, đảm bảo tư thế trị liệu, chăm
Bài 15
sóc loét do tì đè, vận động sớm, duy trì tầm
Điều
vận động, xử lí liệt nửa người, xử lí mất cảm
trị
giác, tạo thuận chức năng chi trên, làm
phục
mạnh cơ, cải thiện dáng đi, thăng bằng và di
hồi
chuyển, khuyến khích độc lập các sinh hoạt
vận
15 35 5 0 30
hàng ngày
động
sau
2. Đề xuất chính xác chỉ định kết hợp 11
đột
chiến lược trong điều trị phục hồi vận động.
quỵ
3. Thành thạo áp dụng các phương pháp
điều trị phục hồi vận động sau đột quỵ mang
lại hiệu quả tích cực
Bài 16
1. Trình bày đầy đủ các tổn thương của bệnh
Chẩn
mạch máu tủy/ Trình bày đầy đủ các chỉ
đoán,
định và quy trình can thiệp điều trị các bệnh
phân
mạch máu tuỷ.
loại
và 2. Đủ điều kiện thực hiện được các kỹ thuật
16 điều can thiệp các bệnh mạch máu tuỷ trên bệnh 45 5 0 40
trị nhân trong phòng can thiệp DSA.
bệnh
9
--- PAGE 10 ---
mạch
3. Thể hiện được sự thận trọng khi can thiệp
máu
để tránh tai biến xảy ra.
tủy
Bài 17
1. Trình bày đầy đủ các tiêu chuẩn chẩn
Chẩn
đoán & quy trình can thiệp điều trị dị dạng
đoán
thông động tĩnh mạch Galen ở trẻ em.
điều
trị dị 2. Đủ điều kiện và thực hiện được kỹ thuật
dạng can thiệp dị dạng thông động tĩnh mạch
thông Galen ở trẻ em trong phòng can thiệp DSA
17
động
3. Thể hiện được sự thận trọng khi can thiệp
tĩnh 35 5 0 30
để tránh tai biến xảy ra.
mạch
Galen
ở trẻ
em
1. Trình bày đầy đủ các tiêu chuẩn và cách
phân loại u máu sơ sinh, u máu bẩm sinh, u
máu dạng chùm, u nội mô mạch máu dạng
Bài 18 Kaposi, u nội mô mạch máu hình thoi, u nội
Phân mô mạch máu biểu mô, các dạng u nội mô
loại mạch máu hiếm gặp khác như: hỗn hợp,
“u dạng lưới…,sang thương da như u máu mao
máu” mạch dạng thùy hạt sinh mủ, sarcom mạch
và máu
điều
2. Đủ điều kiện và thực hiện được kỹ thuật
18 trị can 50 10 10 30
can thiệp một số trường hợp u máu trên
thiệp
bệnh nhân trong phòng can thiệp DSA
nội
mạch 3. Thể hiện được sự thận trọng khi phân
loại, áp dụng phương pháp can thiệp để
tránh tai biến xảy ra.
Tổng cộng 860 75 109 676
Ôn tập & kiểm tra cuối khóa
10
--- PAGE 11 ---
5. TÀI LIỆU
5.1. Tài liệu đào tạo chính
Tài liệu can thiệp mạch máu thần kinh cơ bản và nâng cao do các bác sĩ Bệnh viện Đa
Khoa Quốc Tế S.I.S biên soạn và lưu hành nội bộ.
Sách Chẩn Đoán Và Điều Trị Bệnh Mạch Máu Thần Kinh Và Đột Quỵ, chủ biên: TS.BS
Trần Chí Cường, NXB: Đại học Y dược TP. HCM
5.2. Tài liệu tham khảo cho học viên
1. Anne G. Osborn and et al. (1999), Cerebral angiography, 1st edition, Lippincott
Williams & Wilkins.
2. Anne G. Osborn and et al. (2004), Cerebral angiography, 2nd edition, Lippincott
Williams & Wilkins.
3. Bo Norrving (2014), Oxford textbook of stroke and cerebrovascular disease.
4. Catherine Westbrook (2014), Handbook of MRI Technique, 4th edition, Wiley
Blackwell.
5. Catherine Westbrook (2010), MRI at a Glance, Wiley Blackwell.
6. Catherine Westbrook, Carolyn Kaut Roth and John Talbot (2011), MRI in practice,
4th edition, Blackwell Publishing.
7. Erskine J. Holmes, Anna C. Forest-Hay and Rakesh R. Misra (2008), Interpretation
of emergency head CT a practical handbook, Cambridge University Press.
8. Neil M. Borden and Jay K. Costantini (2007), 3D angiographic atlas of
neurovascular anatomy and pathology, Cambridge University.
9. AHA/ASA guideline (2013), Guidelines for the early management of patients with
acute ischemic stroke.
10. AHA/ASA guideline (2015), 2015 AHA/ASA focused update of the 2013
guidelines for the early management of patients with acute ischemic stroke
regarding endovascular treatment.
6. PHƯƠNG PHÁP DẠY, HỌC
Phương pháp dạy học trong chương trình đào tạo này sẽ thực hiện theo nguyên tắc
dạy học tích cực trong y học. Học viên là: trung tâm, chủ động, tích cực tham gia hoạt động
trong lớp. Giảng viên là: chủ đạo, là người tổ chức các hoạt động dạy học trong lớp. Đánh
giá kết quả học tập là học viên biết được gì, làm được gì sau mỗi bài học.
Các phương pháp dạy học sử dụng chủ yếu trong chương trình này gồm:
- Thuyết trình minh họa, thuyết trình ngắn, tích cực hóa học viên;
- Dạy học bằng tình huống lâm sàng/ dạy học trên giường bệnh;
- Đọc tài liệu;
- Dạy học bằng thao tác mẫu thực hành lại dựa trên bảng kiểm;
- Dạy học bằng phương pháp cầm tay chỉ việc trên lâm sàng;
- Dạy học bằng thảo luận nhóm, làm việc nhóm;
11
--- PAGE 12 ---
7. TIÊU CHUẨN CHỌN GIẢNG VIÊN VÀ TRỢ GIẢNG
7.1. Tiêu chuẩn chọn giảng viên
Giảng viên là bác sỹ có bằng sau đại học chuyên khoa Tim mạch; Thần kinh; Chẩn
đoán hình ảnh; Ngoại khoa và Can thiệp mạch máu thần kinh; có tối thiểu 5 năm kinh
nghiệm trong nghề
Có chứng chỉ Sư phạm y học cơ bản theo quy định của Thông tư 22-BYT.
7.2. Tiêu chuẩn chọn trợ giảng
Trợ giảng là các bác sỹ chuyên khoa Tim mạch; Thần kinh; Chẩn đoán hình ảnh; Ngoại
khoa và Can thiệp mạch máu thần kinh; có tối thiểu 3 năm kinh nghiệm trong nghề
Có chứng chỉ Sư phạm y học cơ bản theo quy định của Thông tư 22-BYT.
7.3. Số lượng giảng viên và trợ giảng
 Lý thuyết: 1 giảng viên và 1 trợ giảng một buổi giảng.
 Thực hành: 1 giảng viên hoặc 1 trợ giảng mỗi nhóm thực hành
8. THIẾT BỊ VÀ HỌC LIỆU DẠY HỌC
 Máy tính, máy chiếu.
 Các phương tiện chẩn đoán hình ảnh: CT; MRI; DSA.
 Hệ thống PACs.
 Thăm khám bệnh nhân tại các khoa phòng.
 Tham gia kiến tập, phụ chụp và can thiệp mạch máu thần kinh tại phòng can thiệp DSA
 Tham gia trực gác.
9. HƯỚNG DẪN TỔ CHỨC THỰC HIỆN CHƯƠNG TRÌNH
9.1. Cách thức tuyển sinh
 Tuyển sinh qua thông báo tới các bệnh viện.
 Số lượng học viên: không quá 10 học viên.
9.2. Thời gian đào tạo và địa điểm:
 Khoá học kéo dài trong 6 tháng linh hoạt, với 80 tiết lí thuyết và 780 tiết thực hành
9.3. Địa điểm
 Khóa học được tổ chức tại Bệnh viện Đa khoa Quốc tế S.I.S Cần Thơ.
9.4. Tổ chức đào tạo
- Học lý thuyết: hội trường tầng 8 Bệnh viện Đa khoa Quốc Tế S.I.S Cần Thơ.
- Học thực hành: các khoa phòng (Chẩn đoán hình ảnh; Thần kinh đột quỵ; Nội tổng
hợp; Cấp cứu; ICU và Đơn vị DSA).
- Thực hành chia nhóm nhỏ, mỗi nhóm 3-5 học viên
9.5. Cách thức tổ chức lượng giá lý thuyết và thực hành
 Thi lý thuyết bằng trắc nghiệm khách quan tại hội trường tầng 8 Bệnh viện Đa khoa
Quốc Tế S.I.S Cần Thơ, số lượng giảng viên: 01.
12
--- PAGE 13 ---
 Thi thực hành bằng cách bốc thăm ngẫu nhiên để chọn một kỹ năng đã học tại
phòng DSA Bệnh viện Đa khoa Quốc Tế S.I.S Cần Thơ, số lượng giảng viên: 02 03.
10. ĐÁNH GIÁ VÀ CẤP GIẤY CHỨNG CHỈ ĐÀO TẠO
10.1. Đánh giá
 Đánh giá thường xuyên: giảng viên sẽ kiểm tra kiến thức của học viên cả lý thuyết
và thực hành vào giữa khóa học.
 Đánh giá kết thúc
 Thi lý thuyết: thi tự luận về lý thuyết và giải quyết tình huống trong 90 phút.
 Thi thực hành: tại phòng DSA bằng kỹ năng thực hành trên bệnh nhân và các câu
hỏi liên quan trong quá trình thực hành. Thời lượng 60-90 phút cho mỗi học viên.
 Cách tính điểm kết thúc (tất cả các bài thi lý thuyết và thực hành giữa khóa và cuối
khóa đều được tính bằng thang điểm 10)
 Điểm thi lý thuyết = (điểm thi lý thuyết cuối khóa x 7 + điểm thi lý thuyết giữa
khóa x 3)/10.
 Điểm thi thực hành = (điểm thi thực hành cuối khóa x 7 + điểm thi thực hành giữa
khóa x 3)/10.
 Điểm kết thúc cuối khóa = (Điểm thi lý thuyết + Điểm thi thực hành x 2)/3.
- Chuyên cần
 Vắng lý thuyết ≤ 10%.
 Vắng mặt thực hành 0% (nếu vắng bắt buộc phải học bù)
10.2. Cấp chứng chỉ đào tạo
 Tên chứng chỉ: Can thiệp mạch máu thần kinh nâng cao
 Điều kiện cấp chứng chỉ: Điểm kiểm tra kết thúc: từ đạt trở lên (điểm kết thúc cuối
khóa  7 điểm).
10.3. Giá trị của chứng chỉ
 Có giá trị trong việc duy trì chứng chỉ hành nghề trong phạm vị khám chữa bệnh
chuyên khoa Nội thần kinh, Ngoại thần kinh.
 Có thể tham gia đứng tên các thủ thuật, phẫu thuật chụp và can thiệp mạch máu
thần kinh chuyên sâu.
13
--- PAGE 14 ---
11. BẢNG CHỈ TIÊU VỀ KĨ NĂNG
Chỉ tiêu tay nghề (số
trường hợp tối thiểu/1 hv)
Tên bài Kỹ năng/ thủ thuật
Lâm sàng
Lab
Kiến Trợ Tự
tập giúp làm
Bài 1 Đọc và phân tích chính xác các mốc giải phẫu
Hình ảnh học CT, não, tủy trên hình ảnh CT 5 5 5 10
MRI sọ não Đọc và phân tích chính xác các mốc giải phẫu
não, tủy trên hình ảnh MRI 10 10 10 10
Đọc và phân tích cụ thể hình ảnh bất thường
trên CT scan trong chẩn đoán nhồi máu não 5 5 5 5
Đọc và phân tích cụ thể hình ảnh bất thường
Bài 2 trên MRI trong chẩn đoán nhồi máu não 10 10 10 10
Hình ảnh học
Phân tích đầy đủ tổn thương trên nhu mô và
trong chẩn đoán
trên mạch máu trên hình chụp CTA trong chẩn
nhồi máu não 5 5 5 5
đoán nhồi máu não
Phân tích đầy đủ tổn thương trên nhu mô và trên
mạch máu trên hình chụp MRA trong chẩn đoán 10 10 10 10
nhồi máu não
Đọc và phân tích chính xác hình ảnh bất thường
trên CT scan trong chẩn đoán xuất huyết não 5 5 5 5
Đọc và phân tích chính xác hình ảnh bất thường
Bài 3 trên MRI trong chẩn đoán xuất huyết não 10 10 10 10
Hình ảnh học
Phân tích đầy đủ tổn thương trên nhu mô và
trong chẩn đoán trên mạch máu trên hình chụp CTA trong chẩn
5 5 5 5
xuất huyết não đoán xuất huyết não
Phân tích đầy đủ tổn thương trên nhu mô và
trên mạch máu trên hình chụp MRA trong chẩn
10 10 10 10
đoán xuất huyết não
14
--- PAGE 15 ---
Đọc và phân tích chính xác hình ảnh bất thường
5 5 5 5
trên CT scan trong chẩn đoán bệnh mạch máu tủy
Đọc và phân tích chính xác hình ảnh bất thường
Bài 4 10 10 10 10
trên MRI trong chẩn đoán bệnh mạch máu tủy
Hình ảnh học
Phân tích đầy đủ tổn thương trên nhu mô và
trong chẩn đoán
trên mạch máu trên hình chụp CTA trong chẩn 5 5 5 5
bệnh mạch máu
đoán bệnh mạch máu tủy
tủy
Phân tích đầy đủ tổn thương trên nhu mô và trên
mạch máu trên hình chụp MRA trong chẩn đoán 10 10 10 10
bệnh mạch máu tủy
Phân tích và kết luận chính xác phương pháp
điều trị tổng quát & điều trị các biến chứng cấp 5 5 5 10
trong chẩn đoán điều trị nội khoa nhồi máu não
Phân tích và kết luận chính xác phương pháp
điều trị kháng đông trong chẩn đoán điều trị nội 5 5 5 10
Bài 5
khoa nhồi máu não
Chẩn đoán điều trị
nội khoa nhồi Phân tích và kết luận chính xác phương pháp
máu não điều trị tăng thể tích, dãn mạch & tăng huyết áp 5 5 5 10
trong chẩn đoán điều trị nội khoa nhồi máu não
Học viên sẽ được thực
Phân tích và kết luận chính xác phương pháp
hành trong bài "Nhồi máu
điều trị tiêu sợi huyết trong chẩn đoán điều trị
não cấp: chỉ định điều trị
nội khoa nhồi máu não
tiêu sợi huyết tĩnh mạch"
Bài 6 Nhồi máu
Thành thạo chỉ định & thực hiện được kĩ
não cấp: chỉ định
thuật bơm tiêu sợi huyết tĩnh mạch trên bệnh
điều trị tiêu sợi
nhân lâm sàng tại khoa Cấp cứu của bệnh
huyết tĩnh mạch 10 10 10 15
viện
Thực hiện thành thạo kỹ thuật lấy huyết khối bằng
Bài 7 Nhồi máu
stent trên bệnh nhân.
não cấp: chỉ định
5 5 5 10
điều trị nội mạch
lấy huyết khối Thực hiện thành thạo kỹ thuật hút huyết khối trên
bằng dụng cụ trên bệnh nhân.
5 5 5 10
15
--- PAGE 16 ---
Thành thạo chỉ định phẫu thuật mở sọ giải áp
Bài 8
trong nhồi máu não khi điều trị cho BN trong
Điều trị phẫu 0 10 10 10
trường hợp phải hội chẩn với các bác sĩ Ngoại
thuật mở sọ giải
thần kinh tại bệnh viện
áp trong nhồi máu
não Thực hành điều trị phẫu thuật mở sọ giải áp
trong nhồi máu não 0 10 0 0
Bài 9 Chẩn đoán Thực hành chỉ định điều trị phù hợp ở bệnh
điều trị nội khoa nhân xuất huyết não-màng não và thực hiện
xuất huyết não được điều trị nội khoa xuất huyết não- màng 0 10 10 30
màng não não theo đúng quy trình kĩ thuật tại bệnh viện
Thực hành chỉ định phẫu thuật trong xuất huyết
Bài 10 Điều trị
não-màng não/ Đánh giá được tiên lượng, kết
phẫu thuật trong 0 10 10 10
quả và các biến chứng phẫu thuật xuất huyết
xuất huyết não
não màng não.
màng não
Thực hành điều trị phẫu thuật trong xuất huyết
0 10 0 0
não màng não
Thành thạo thực hiện can thiệp và đứng chính
Bài 11 Can thiệp can thiệp kỹ thuật gây tắc Nidus bằng keo 0 10 10 10
nội mạch điều trị dị Cyanoarylate điều trị dị dạng mạch máu não
dạng mạch máu não
Thành thạo thực hiện can thiệp và đứng chính
can thiệp kỹ thuật gây tắc trong Nidus với các
tác nhân không dính điều trị dị dạng mạch máu 0 10 10 10
Não.
Bài 12 Can thiệp
Thành thạo thực hiện can thiệp và đứng chính
nội mạch điều trị
can thiệp điều trị các trường hợp phình động
phình động mạch
0 10 15 20
mạch não bằng các phương pháp phù hợp
não
Thành thạo thực hiện can thiệp và đứng chính can
Bài 13 Chẩn đoán
thiệp điều trị rò động mạch cảnh xoang hang bằng
& điều trị rò động
bóng 0 10 10 10
mạch cảnh xoang
hang
Thành thạo thực hiện can thiệp và đứng chính can
thiệp điều trị rò động mạch cảnh xoang hang bằng
0 10 10 10
cách đặt coil tách rời và coil đẩy
16
--- PAGE 17 ---
Thành thạo thực hiện can thiệp và đứng chính
can thiệp điều trị rò động tĩnh mạch màng cứng
0 10 10 10
bằng phương pháp can thiệp nội mạch gây tắc
tĩnh mạch dẫn lưu
Bài 14 Chẩn đoán
Thành thạo thực hiện chỉ định phương pháp
& điều trị rò động
điều trị bảo tồn trong chẩn đoán điều trị rò động 0 10 5 5
tĩnh mạch màng
tĩnh mạch màng cứng
cứng
Thành thạo thực hiện chỉ định phương pháp
phẫu thuật trong chẩn đoán điều trị rò động tĩnh 0 10 5 5
mạch màng cứng
Thành thạo thực hiện chỉ định phương pháp xạ
phẫu trong chẩn đoán điều trị rò động tĩnh mạch 0 10 5 5
màng cứng
Bài 15 Điều trị
Phân tích đúng và chỉ định kết hợp 11 chiến
phục hồi vận
lược trong điều trị phục hồi vận động phù hợp 0 10 10 30
động sau đột quỵ
đối với từng bệnh nhân cụ thể
Thực hành can thiệp nội mạch điều trị thông nối
Bài 16 Chẩn đoán, động tĩnh mạch màng cứng tuỷ sống 0 10 10 0
phân loại và điều
trị bệnh mạch
Thực hành can thiệp nội mạch điều trị dị dạng
máu tủy
động tĩnh mạch tủy sống 0 10 10 0
Thực hành can thiệp nội mạch gây tắc đường
Bài 17 Chẩn động mạch điều trị dị dạng thông động tĩnh 0 10 10 0
đoán điều trị dị mạch Galen ở trẻ em
dạng thông động
Thực hành can thiệp nội mạch sử dụng coil bít
tĩnh mạch Galen
lỗ rò đường tĩnh mạch điều trị dị dạng thông
ở trẻ em
động tĩnh mạch Galen ở trẻ em 0 10 10 0
Bài 18 Thực hiện chỉ định phương pháp điều trị u
10 5 0 0
Phân loại “u máu bẩm sinh
17
--- PAGE 18 ---
máu” và điều trị Thực hiện chỉ định phương pháp điều trị u
10 5 0 0
can thiệp nội máu sơ sinh
mạch
Thực hiện chỉ định phương pháp điều trị u
10 5 0 0
hạt sinh mủ
Thực hiện chỉ định phương pháp điều trị u
10 5 0 0
nội mô mạch máu dạng kaposi
Thực hiện chỉ định phương pháp điều trị u
10 5 0 0
máu dạng chùm
Thực hiện chỉ định phương pháp điều trị
10 5 0 0
sacrom mạch máu
Thành thạo đứng can thiệp nội mạch trường
10 5 5 5
hợp u máu sơ sinh .
18
--- PAGE 19 ---
PHỤ LỤC 1
DANH SÁCH GIẢNG VIÊN CƠ HỮU VÀ GIẢNG VIÊN THỈNH GIẢNG
DANH SÁCH GIẢNG VIÊN CƠ HỮU
1 TS.BS Trần Chí Cường
2 TS.BS Hà Tấn Đức
3 ThS.BS Nguyễn Lưu Giang
4 BS.CKII Bùi Thị Bích
5 BS.CKII Châu Thị Thúy Liễu
6 BS.CKII Lê Uyển Diễm
7 BS.CKII Ngô Minh Tuấn
8 ThS.BS Huỳnh Quốc Sĩ
9 ThS.BS Nguyễn Mạnh Cường
10 BS.CKI Phương Hồng Thọ
11 ThS.BS Nguyễn Đào Nhật Huy
12 ThS.BS Trần Minh Luận
DANH SÁCH GIẢNG VIÊN THỈNH GIẢNG
1 GS.TS. Phạm Minh Thông
2 TS.BS Đỗ Nguyên Tín
3 TS.BS Huỳnh Hồng Châu
19
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+121
View File
@@ -0,0 +1,121 @@
import pdfplumber
import json
import os
import re
files = [
"/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/2.-CHUONG-TRINH-CAN-THIEP-MACH-MAU-THAN-KINH-NANG-CAO-BO-SUNG-FINAL.pdf",
"/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/CHUONG-TRINH-CAN-THIEP-MACH-MAU-CAC-TANG-VA-MACH-MAU-NGOAI-BIEN-CO-BAN.pdf",
"/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/CHUONG-TRINH-CHUAN-BI-DUNG-CU-VA-CHAM-SOC-BENH-NHAN-TRONG-PHONG-CHUP-MACH.pdf",
"/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/CHUONG-TRINH-DAO-TAO-TIM-MACH-CAN-THIEP-1.pdf"
]
slugs = [
"can-thiep-mach-mau-than-kinh-nang-cao",
"can-thiep-mach-mau-cac-tang-va-ngoai-bien-co-ban",
"chuan-bi-dung-cu-va-cham-soc-benh-nhan-trong-phong-chup-mach",
"tim-mach-can-thiep-co-ban"
]
titles = [
"Can thiệp mạch máu thần kinh nâng cao",
"Can thiệp mạch máu các tạng và mạch máu ngoại biên cơ bản",
"Chuẩn bị dụng cụ và chăm sóc bệnh nhân trong phòng chụp mạch",
"Tim mạch can thiệp cơ bản"
]
def make_text_block(text):
return {
"type": "paragraph",
"data": {
"text": text.replace('\n', '<br>')
}
}
def make_header_block(text, level=3):
return {
"type": "header",
"data": {
"text": text,
"level": level
}
}
def make_tab_start(title):
return {
"type": "tabStart",
"data": {
"title": title
}
}
def make_tab_end():
return {
"type": "tabEnd",
"data": {}
}
os.makedirs('pdf_json_output', exist_ok=True)
for f, slug, title in zip(files, slugs, titles):
blocks = []
blocks.append(make_tab_start("Nội dung"))
with pdfplumber.open(f) as pdf:
for page in pdf.pages:
text = page.extract_text()
if not text:
continue
# Simple heuristic: split by double newlines or single newlines
paragraphs = text.split('\n')
current_p = []
for line in paragraphs:
line = line.strip()
if not line:
continue
# If it looks like a heading
if re.match(r'^(\d+)\.\s+(.*)$', line) or line.isupper():
if current_p:
blocks.append(make_text_block(' '.join(current_p)))
current_p = []
blocks.append(make_header_block(line, level=3))
else:
current_p.append(line)
if current_p:
blocks.append(make_text_block(' '.join(current_p)))
# Try to extract tables
tables = page.extract_tables()
for table in tables:
if not table: continue
# Convert table to HTML for raw block
html = "<table class='table table-bordered'><tbody>"
for row in table:
html += "<tr>"
for cell in row:
cell_text = cell.replace('\n', '<br>') if cell else ''
html += f"<td>{cell_text}</td>"
html += "</tr>"
html += "</tbody></table>"
blocks.append({
"type": "raw",
"data": {
"html": html
}
})
blocks.append(make_tab_end())
editor_data = {
"time": 1700000000000,
"blocks": blocks,
"version": "2.29.1"
}
with open(f"pdf_json_output/{slug}.json", 'w', encoding='utf-8') as out:
json.dump(editor_data, out, ensure_ascii=False)
print(f"Processed {title}")
print("All done!")
+172
View File
@@ -0,0 +1,172 @@
import pdfplumber
import re
import json
import os
files = [
'/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/2.-CHUONG-TRINH-CAN-THIEP-MACH-MAU-THAN-KINH-NANG-CAO-BO-SUNG-FINAL.pdf',
'/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/CHUONG-TRINH-CAN-THIEP-MACH-MAU-CAC-TANG-VA-MACH-MAU-NGOAI-BIEN-CO-BAN.pdf',
'/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/CHUONG-TRINH-CHUAN-BI-DUNG-CU-VA-CHAM-SOC-BENH-NHAN-TRONG-PHONG-CHUP-MACH.pdf',
'/home/x79/sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/DaoTao/CHUONG-TRINH-DAO-TAO-TIM-MACH-CAN-THIEP-1.pdf'
]
slugs = [
"can-thiep-mach-mau-than-kinh-nang-cao",
"can-thiep-mach-mau-cac-tang-va-ngoai-bien-co-ban",
"chuan-bi-dung-cu-va-cham-soc-benh-nhan-trong-phong-chup-mach",
"tim-mach-can-thiep-co-ban"
]
def extract_section(text, start_keywords, end_keywords=None):
lines = text.split('\n')
started = False
content = []
for line in lines:
line_lower = line.lower()
if not started:
for k in start_keywords:
if k in line_lower:
started = True
split_idx = line_lower.find(k) + len(k)
val = line[split_idx:].strip(': ').strip()
if val:
content.append(val)
break
else:
if end_keywords:
for k in end_keywords:
if k in line_lower:
return '\n'.join(content).strip()
# Stop if we hit a new numbered section like "10." or empty lines after some text
if re.match(r'^\d+\.', line) or re.match(r'^[A-Z\s]+$', line):
return '\n'.join(content).strip()
if line.strip():
content.append(line.strip())
return '\n'.join(content).strip()
def make_text_block(text):
return {
"type": "paragraph",
"data": {
"text": text.replace('\n', '<br>')
}
}
def make_header_block(text, level=3):
return {
"type": "header",
"data": {
"text": text,
"level": level
}
}
os.makedirs('pdf_json_output', exist_ok=True)
for f, slug in zip(files, slugs):
print(f"--- Rebuilding {slug} ---")
blocks = []
with pdfplumber.open(f) as pdf:
text = ''
pdf_blocks = []
for page in pdf.pages:
page_text = page.extract_text()
if not page_text:
continue
text += page_text + '\n'
# Simple heuristic: split by double newlines or single newlines
paragraphs = page_text.split('\n')
current_p = []
for line in paragraphs:
line = line.strip()
if not line:
continue
if re.match(r'^(\d+)\.\s+(.*)$', line) or line.isupper():
if current_p:
pdf_blocks.append(make_text_block(' '.join(current_p)))
current_p = []
pdf_blocks.append(make_header_block(line, level=3))
else:
current_p.append(line)
if current_p:
pdf_blocks.append(make_text_block(' '.join(current_p)))
# Try to extract tables
tables = page.extract_tables()
for table in tables:
if not table: continue
html = "<table class='table table-bordered'><tbody>"
for row in table:
html += "<tr>"
for cell in row:
cell_text = cell.replace('\n', '<br>') if cell else ''
html += f"<td>{cell_text}</td>"
html += "</tr>"
html += "</tbody></table>"
pdf_blocks.append({
"type": "raw",
"data": {
"html": html
}
})
# Extraction
time = extract_section(text, ['thời gian đào tạo'])
audience = extract_section(text, ['đối tượng học viên', 'đối tượng tham dự', 'đối tượng'])
classInfo = ""
quantity = extract_section(text, ['số lượng học viên:', 'số lượng học viên'])
documents = extract_section(text, ['hồ sơ đăng ký tham dự', 'hồ sơ nhập học'])
tuitionFee = extract_section(text, ['học phí'])
location = extract_section(text, ['địa điểm nhận hồ sơ'])
paymentMethod = ""
# Tab 1: Tổng quan
blocks.append({"type": "tabStart", "data": {"title": "Tổng quan"}})
if time:
blocks.append(make_header_block("Thời gian", 4))
blocks.append(make_text_block(time))
if audience:
blocks.append(make_header_block("Đối tượng đăng ký", 4))
blocks.append(make_text_block(audience))
if quantity:
blocks.append(make_header_block("Số lượng", 4))
blocks.append(make_text_block(quantity))
if documents:
blocks.append(make_header_block("Hồ sơ đăng ký tham dự", 4))
blocks.append(make_text_block(documents))
# Tab 2: Thông tin chung
blocks.append({"type": "tabStart", "data": {"title": "Thông tin chung"}})
if tuitionFee:
blocks.append(make_header_block("Học phí", 4))
blocks.append(make_text_block(tuitionFee))
if location:
blocks.append(make_header_block("Địa điểm nhận hồ sơ và học phí", 4))
blocks.append(make_text_block(location))
# Tab 3: Chương trình đào tạo
blocks.append({"type": "tabStart", "data": {"title": "Chương trình đào tạo"}})
# Third, add all the extracted text from the PDF
blocks.extend(pdf_blocks)
blocks.append({"type": "tabEnd", "data": {}})
editor_data = {
"time": 1700000000000,
"blocks": blocks,
"version": "2.29.1"
}
with open(f"pdf_json_output/{slug}.json", 'w', encoding='utf-8') as out:
json.dump(editor_data, out, ensure_ascii=False)
print("All done rebuilding json!")
+43
View File
@@ -0,0 +1,43 @@
import os
templates_to_update = [
'src/main/resources/templates/dao-tao-detail.html',
'src/main/resources/templates/posts/full-width.html',
'src/main/resources/templates/posts/livestream.html',
'src/main/resources/templates/posts/sidebar.html',
'src/main/resources/templates/posts/standard.html'
]
# We need to replace:
# groupHtml += '<button class="dao-tao-tab-btn custom-tab-btn ' + activeClass + '" data-tab-idx="' + index + '" style="padding: 10px 20px; border: 1px solid #ccc; background: ' + (index===0 ? '#0055a5' : '#f8f9fa') + '; color: ' + (index===0 ? 'white' : 'black') + '; cursor: pointer;">' + tab.title + '</button>';
# with:
# groupHtml += '<button class="dao-tao-tab-btn custom-tab-btn ' + activeClass + '" data-tab-idx="' + index + '">' + tab.title + '</button>';
# And remove:
# b.style.background = '#f8f9fa';
# b.style.color = 'black';
# btn.style.background = '#0055a5';
# btn.style.color = 'white';
for filepath in templates_to_update:
if not os.path.exists(filepath):
continue
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# Replace the button HTML generator
target_str = """groupHtml += '<button class="dao-tao-tab-btn custom-tab-btn ' + activeClass + '" data-tab-idx="' + index + '" style="padding: 10px 20px; border: 1px solid #ccc; background: ' + (index===0 ? '#0055a5' : '#f8f9fa') + '; color: ' + (index===0 ? 'white' : 'black') + '; cursor: pointer;">' + tab.title + '</button>';"""
new_str = """groupHtml += '<button class="dao-tao-tab-btn custom-tab-btn ' + activeClass + '" data-tab-idx="' + index + '">' + tab.title + '</button>';"""
content = content.replace(target_str, new_str)
# Remove the JS inline styles logic
content = content.replace("b.style.background = '#f8f9fa';", "")
content = content.replace("b.style.color = 'black';", "")
content = content.replace("btn.style.background = '#0055a5';", "")
content = content.replace("btn.style.color = 'white';", "")
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Updated JS in {filepath}")
Binary file not shown.
@@ -0,0 +1,110 @@
package com.sisvietnamvn.web;
import com.sisvietnamvn.web.domain.Category;
import com.sisvietnamvn.web.domain.PageStatus;
import com.sisvietnamvn.web.domain.Post;
import com.sisvietnamvn.web.domain.PostLayout;
import com.sisvietnamvn.web.repository.CategoryRepository;
import com.sisvietnamvn.web.repository.PostRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.Optional;
@Component
public class NoiSoiCourseSeeder implements CommandLineRunner {
private static final Logger LOG = LoggerFactory.getLogger(NoiSoiCourseSeeder.class);
private final CategoryRepository categoryRepository;
private final PostRepository postRepository;
public NoiSoiCourseSeeder(CategoryRepository categoryRepository, PostRepository postRepository) {
this.categoryRepository = categoryRepository;
this.postRepository = postRepository;
}
@Override
public void run(String... args) throws Exception {
LOG.info("=================================================================");
LOG.info("NOI SOI COURSE SEEDER: RUNNING AUTOMATIC DATA SEEDING...");
LOG.info("=================================================================");
try {
// 1. Find or create the Category 'dao-tao'
String categorySlug = "dao-tao";
Category category = categoryRepository.findBySlug(categorySlug).orElseGet(() -> {
Category cat = new Category();
// Generate id dynamically
Long nextId = categoryRepository.count() + 1;
cat.setId(nextId);
cat.setName("Đào tạo");
cat.setSlug(categorySlug);
cat.setDescription("Chương trình đào tạo y khoa");
cat.setCreatedBy("system");
cat.setCreatedDate(Instant.now());
cat = categoryRepository.save(cat);
LOG.info("Created new Category 'dao-tao' with ID: {}", cat.getId());
return cat;
});
// 2. Read content from parsed files
String contentPath = "/tmp/course_content.html";
String excerptPath = "/tmp/course_excerpt.html";
if (!Files.exists(Paths.get(contentPath)) || !Files.exists(Paths.get(excerptPath))) {
LOG.warn("Seed content files do not exist at /tmp. Skipping seeder.");
return;
}
String content = Files.readString(Paths.get(contentPath));
String excerpt = Files.readString(Paths.get(excerptPath));
// 3. Find or create the Post (slug: test-dao-tao)
String postSlug = "test-dao-tao";
Optional<Post> postOpt = postRepository.findBySlug(postSlug);
Post post;
if (postOpt.isPresent()) {
post = postOpt.get();
LOG.info("Found existing Post with slug 'test-dao-tao', updating fields...");
} else {
post = new Post();
// Generate id dynamically
Long nextId = postRepository.count() + 1000L;
post.setId(nextId);
post.setSlug(postSlug);
post.setCreatedBy("system");
post.setCreatedDate(Instant.now());
LOG.info("Creating new Post with slug 'test-dao-tao' (ID: {})", post.getId());
}
post.setTitle("CHƯƠNG TRÌNH CHỨNG CHỈ ĐÀO TẠO KỸ THUẬT CHUYÊN MÔN: NỘI SOI DẠ DÀY, NỘI SOI ĐẠI TRÀNG VÀ NỘI SOI ĐIỀU TRỊ CƠ BẢN, KHÓA 02");
post.setContent(content + "\n<!-- TAB_SPLIT -->\n" + excerpt);
post.setExcerpt("Xem chi tiết thông tin khóa học ở tab bên cạnh");
post.setLocation("Giảng đường 3A, lầu 3, khu A, Bệnh viện Đại học Y Dược Thành phố Hồ Chí Minh (215 Hồng Bàng, Phường Chợ Lớn, Thành phố Hồ Chí Minh)");
post.setMetaDescription("38.000.000 VNĐ");
post.setEventTime(Instant.parse("2026-07-27T07:30:00Z"));
post.setStatus(PageStatus.PUBLISHED);
post.setLayout(PostLayout.TRAINING);
post.setCategory(category);
post.setFeaturedImage("https://console.bvdaihoc.com.vn/uploads/KHDT/T%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026/07/N%E1%BB%98I-SOI-D%E1%BA%A0-D%C3%80Y,-N%E1%BB%98I-SOI-%C4%90%E1%BA%A0I-TR%C3%80NG-V%C3%80-N%E1%BB%98I-SOI-%C4%90I%E1%BB%80U-TR%E1%BB%8A-C%C6%A0-B%E1%BA%A2N-(1).gif");
postRepository.save(post);
LOG.info("=================================================================");
LOG.info("NOI SOI COURSE SEEDED SUCCESSFULLY!");
LOG.info("URL: http://localhost:8080/post/test-dao-tao");
LOG.info("=================================================================");
} catch (Exception e) {
LOG.error("Failed to seed Noi Soi course data: ", e);
}
}
}
@@ -0,0 +1,79 @@
package com.sisvietnamvn.web.config;
import com.sisvietnamvn.web.domain.Post;
import com.sisvietnamvn.web.domain.PageStatus;
import com.sisvietnamvn.web.repository.PostRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.Optional;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import com.fasterxml.jackson.databind.ObjectMapper;
@Component
public class PostMigrator implements CommandLineRunner {
private final PostRepository postRepository;
public PostMigrator(PostRepository postRepository) {
this.postRepository = postRepository;
}
@Override
public void run(String... args) throws Exception {
String[] slugs = {
"can-thiep-mach-mau-than-kinh-nang-cao",
"can-thiep-mach-mau-cac-tang-va-ngoai-bien-co-ban",
"chuan-bi-dung-cu-va-cham-soc-benh-nhan-trong-phong-chup-mach",
"tim-mach-can-thiep-co-ban"
};
String[] titles = {
"Can thiệp mạch máu thần kinh nâng cao",
"Can thiệp mạch máu các tạng và mạch máu ngoại biên cơ bản",
"Chuẩn bị dụng cụ và chăm sóc bệnh nhân trong phòng chụp mạch",
"Tim mạch can thiệp cơ bản"
};
System.out.println("=== STARTING MIGRATION FOR 4 COURSES ===");
for (int i = 0; i < slugs.length; i++) {
String slug = slugs[i];
String title = titles[i];
String path = "pdf_json_output/" + slug + ".json";
if (!Files.exists(Paths.get(path))) {
System.out.println("File not found: " + path);
continue;
}
String json = new String(Files.readAllBytes(Paths.get(path)));
Optional<Post> opt = postRepository.findBySlug(slug);
Post post;
if (opt.isPresent()) {
post = opt.get();
System.out.println("Updating post: " + post.getTitle());
} else {
System.out.println("Creating new post: " + title);
post = new Post();
post.setSlug(slug);
post.setTitle(title);
post.setStatus(PageStatus.PUBLISHED);
post.setCreatedDate(Instant.now());
post.setLastModifiedDate(Instant.now());
post.setCreatedBy("admin");
post.setLastModifiedBy("admin");
post.setExcerpt("Khóa học " + title + " tại Bệnh viện Đa khoa Quốc tế S.I.S Cần Thơ.");
post.setEventTime(Instant.now().plus(30 + i * 10, ChronoUnit.DAYS)); // dummy start date
}
post.setContent(json);
postRepository.save(post);
}
System.out.println("=== SUCCESSFULLY MIGRATED 4 COURSES ===");
}
}
@@ -23,6 +23,9 @@ import com.sisvietnamvn.web.hook.HookManager;
import com.sisvietnamvn.web.security.AuthoritiesConstants;
import com.sisvietnamvn.web.security.SecurityUtils;
import com.sisvietnamvn.web.service.PageService;
import com.sisvietnamvn.web.domain.Post;
import com.sisvietnamvn.web.domain.PageStatus;
import com.sisvietnamvn.web.repository.PostRepository;
/**
* Controller for rendering public-facing pages dynamically.
@@ -41,12 +44,14 @@ public class PageController {
private final com.sisvietnamvn.web.service.DoctorApiSyncService doctorApiSyncService;
private final com.sisvietnamvn.web.service.SettingService settingService;
private final com.sisvietnamvn.web.repository.DoctorScheduleRepository doctorScheduleRepository;
private final PostRepository postRepository;
public PageController(PageService pageService, ObjectMapper objectMapper, HookManager hookManager, com.sisvietnamvn.web.service.HtmlSnippetService snippetService,
com.sisvietnamvn.web.service.DoctorService doctorService, com.sisvietnamvn.web.service.SpecialtyService specialtyService,
com.sisvietnamvn.web.service.DoctorApiSyncService doctorApiSyncService,
com.sisvietnamvn.web.service.SettingService settingService,
com.sisvietnamvn.web.repository.DoctorScheduleRepository doctorScheduleRepository) {
com.sisvietnamvn.web.repository.DoctorScheduleRepository doctorScheduleRepository,
PostRepository postRepository) {
this.pageService = pageService;
this.objectMapper = objectMapper;
this.hookManager = hookManager;
@@ -56,6 +61,7 @@ public class PageController {
this.doctorApiSyncService = doctorApiSyncService;
this.settingService = settingService;
this.doctorScheduleRepository = doctorScheduleRepository;
this.postRepository = postRepository;
}
@GetMapping("/page/{slug}")
@@ -81,10 +87,48 @@ public class PageController {
public String getAboutUs(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.ABOUT_US), model); }
@GetMapping("/dao-tao")
@org.springframework.transaction.annotation.Transactional(readOnly = true)
public String getDaoTao(Model model) {
java.util.List<Post> daoTaoPosts = postRepository.findAll().stream()
.filter(p -> p.getStatus() == PageStatus.PUBLISHED)
.filter(p -> (p.getCategory() != null && "dao-tao".equalsIgnoreCase(p.getCategory().getSlug())) ||
java.util.Arrays.asList("can-thiep-mach-mau-than-kinh-nang-cao", "can-thiep-mach-mau-cac-tang-va-ngoai-bien-co-ban", "chuan-bi-dung-cu-va-cham-soc-benh-nhan-trong-phong-chup-mach", "tim-mach-can-thiep-co-ban").contains(p.getSlug()))
.sorted((p1, p2) -> {
if (p1.getCreatedDate() == null && p2.getCreatedDate() == null) return 0;
if (p1.getCreatedDate() == null) return 1;
if (p2.getCreatedDate() == null) return -1;
return p2.getCreatedDate().compareTo(p1.getCreatedDate());
})
.collect(java.util.stream.Collectors.toList());
model.addAttribute("daoTaoPosts", daoTaoPosts);
return "dao-tao";
}
@GetMapping("/dao-tao/{slug}")
public String getDaoTaoDetail(@PathVariable("slug") String slug, Model model) {
Post post = postRepository.findBySlug(slug)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
if (post.getStatus() != PageStatus.PUBLISHED) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
// Apply WordPress-style Content Filters
post.setTitle(hookManager.applyFilters("the_title", post.getTitle()));
post.setContent(hookManager.applyFilters("the_content", post.getContent()));
// Format date for display
if (post.getCreatedDate() != null) {
String formattedDate = java.time.format.DateTimeFormatter.ofPattern("dd/MM/yyyy")
.withZone(java.time.ZoneId.systemDefault())
.format(post.getCreatedDate());
model.addAttribute("formattedDate", formattedDate);
}
model.addAttribute("post", post);
return "dao-tao-detail";
}
@GetMapping("/specialty")
public String getSpecialty(Model model) { return renderPage(pageService.findByPageType(com.sisvietnamvn.web.domain.PageType.SPECIALTY), model); }
@@ -4,6 +4,10 @@ import com.sisvietnamvn.web.domain.PageStatus;
import com.sisvietnamvn.web.domain.Post;
import com.sisvietnamvn.web.repository.PostRepository;
import com.sisvietnamvn.web.hook.HookManager;
import com.sisvietnamvn.web.service.HtmlSnippetService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
@@ -11,17 +15,27 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.server.ResponseStatusException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.List;
@Controller
@RequestMapping("/post")
public class PostController {
private static final Logger LOG = LoggerFactory.getLogger(PostController.class);
private final PostRepository postRepository;
private final HookManager hookManager;
private final ObjectMapper objectMapper;
private final HtmlSnippetService snippetService;
public PostController(PostRepository postRepository, HookManager hookManager) {
public PostController(PostRepository postRepository, HookManager hookManager, ObjectMapper objectMapper, HtmlSnippetService snippetService) {
this.postRepository = postRepository;
this.hookManager = hookManager;
this.objectMapper = objectMapper;
this.snippetService = snippetService;
}
@GetMapping("/{slug}")
@@ -36,7 +50,30 @@ public class PostController {
// Apply WordPress-style Content Filters
post.setTitle(hookManager.applyFilters("the_title", post.getTitle()));
post.setContent(hookManager.applyFilters("the_content", post.getContent()));
String content = hookManager.applyFilters("the_content", post.getContent());
if (content != null && !content.trim().isEmpty()) {
try {
Map<String, Object> editorData = objectMapper.readValue(content, new TypeReference<>() {});
if (editorData.containsKey("blocks")) {
List<Map<String, Object>> blocks = (List<Map<String, Object>>) editorData.get("blocks");
for (Map<String, Object> block : blocks) {
if ("snippet".equals(block.get("type"))) {
Map<String, Object> data = (Map<String, Object>) block.get("data");
if (data != null && data.containsKey("id")) {
String snippetId = (String) data.get("id");
data.put("htmlContent", snippetService.getSnippetContent(snippetId));
}
}
}
content = objectMapper.writeValueAsString(editorData);
}
} catch (JsonProcessingException e) {
LOG.error("Failed to parse Editor.js JSON for post ID: {}", post.getId(), e);
}
}
post.setContent(content);
if (post.getExcerpt() != null) {
post.setExcerpt(hookManager.applyFilters("the_excerpt", post.getExcerpt()));
}
@@ -51,16 +88,29 @@ public class PostController {
model.addAttribute("formattedDate", formattedDate);
}
switch (post.getLayout()) {
case SIDEBAR:
return "posts/sidebar";
case FULL_WIDTH:
return "posts/full-width";
case LIVESTREAM:
return "posts/livestream";
case STANDARD:
default:
return "posts/standard";
// Determine which template to use based on layout (with null-safe handling)
com.sisvietnamvn.web.domain.PostLayout layout = post.getLayout();
// Fallback: if no layout is set, check category to auto-detect training posts
if (layout == null && post.getCategory() != null && "dao-tao".equalsIgnoreCase(post.getCategory().getSlug())) {
layout = com.sisvietnamvn.web.domain.PostLayout.TRAINING;
}
if (layout != null) {
switch (layout) {
case SIDEBAR:
return "posts/sidebar";
case FULL_WIDTH:
return "posts/full-width";
case LIVESTREAM:
return "posts/livestream";
case TRAINING:
return "dao-tao-detail";
case STANDARD:
default:
return "posts/standard";
}
}
return "posts/standard";
}
}
@@ -0,0 +1,26 @@
package com.sisvietnamvn.web.controller;
import com.sisvietnamvn.web.NoiSoiCourseSeeder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SeederController {
private final NoiSoiCourseSeeder seeder;
public SeederController(NoiSoiCourseSeeder seeder) {
this.seeder = seeder;
}
@GetMapping("/api/seed-dao-tao")
public String seed() {
try {
seeder.run();
return "Seeding completed successfully! Check the console logs for details.";
} catch (Exception e) {
e.printStackTrace();
return "Seeding failed: " + e.getMessage();
}
}
}
@@ -8,5 +8,6 @@ public enum PostLayout {
SIDEBAR,
FULL_WIDTH,
EVENT,
LIVESTREAM
LIVESTREAM,
TRAINING
}
@@ -3,7 +3,9 @@ package com.sisvietnamvn.web.service;
import com.sisvietnamvn.web.domain.Media;
import com.sisvietnamvn.web.domain.Page;
import com.sisvietnamvn.web.domain.Post;
import com.sisvietnamvn.web.domain.PostLayout;
import com.sisvietnamvn.web.domain.PageStatus;
import com.sisvietnamvn.web.repository.CategoryRepository;
import com.sisvietnamvn.web.repository.MediaRepository;
import com.sisvietnamvn.web.repository.PageRepository;
import com.sisvietnamvn.web.repository.PostRepository;
@@ -39,11 +41,13 @@ public class ImportExportService {
private final PostRepository postRepository;
private final PageRepository pageRepository;
private final MediaRepository mediaRepository;
private final CategoryRepository categoryRepository;
public ImportExportService(PostRepository postRepository, PageRepository pageRepository, MediaRepository mediaRepository) {
public ImportExportService(PostRepository postRepository, PageRepository pageRepository, MediaRepository mediaRepository, CategoryRepository categoryRepository) {
this.postRepository = postRepository;
this.pageRepository = pageRepository;
this.mediaRepository = mediaRepository;
this.categoryRepository = categoryRepository;
}
/**
@@ -249,6 +253,34 @@ public class ImportExportService {
}
}
// Parse categories
NodeList catNodes = el.getElementsByTagName("category");
for (int j = 0; j < catNodes.getLength(); j++) {
Node catNode = catNodes.item(j);
if (catNode.getNodeType() == Node.ELEMENT_NODE) {
Element catEl = (Element) catNode;
String domain = catEl.getAttribute("domain");
if ("category".equals(domain)) {
String catSlug = catEl.getAttribute("nicename");
String catName = catEl.getTextContent();
if (catSlug != null && !catSlug.isEmpty()) {
com.sisvietnamvn.web.domain.Category category = categoryRepository.findBySlug(catSlug).orElseGet(() -> {
com.sisvietnamvn.web.domain.Category newCat = new com.sisvietnamvn.web.domain.Category();
newCat.setSlug(catSlug);
newCat.setName(catName);
return categoryRepository.save(newCat);
});
post.setCategory(category);
// Auto-assign TRAINING layout for dao-tao category
if ("dao-tao".equalsIgnoreCase(catSlug)) {
post.setLayout(PostLayout.TRAINING);
}
break; // just use the first category found
}
}
}
}
postRepository.save(post);
}
} else if ("page".equalsIgnoreCase(postType)) {
@@ -90,15 +90,19 @@ body, h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
}
.cc--tabbed-media-content .tab-labels-inner {
border-bottom-color: #cccccc !important; /* light gray line for the tab row */
border-bottom-color: #cccccc !important;
/* light gray line for the tab row */
}
.cc--tabbed-media-content .tab-label {
color: #555555 !important; /* dark gray for inactive tabs */
color: #555555 !important;
/* dark gray for inactive tabs */
font-weight: 700 !important;
}
.cc--tabbed-media-content .tab-label[aria-selected="true"] {
color: #000000 !important; /* solid black for active tab */
border-bottom: 3px solid #000000 !important; /* solid black underline for active tab */
color: #000000 !important;
/* solid black for active tab */
border-bottom: 3px solid #000000 !important;
/* solid black underline for active tab */
}
.cc--tabbed-media-content .tabbed-media-content-media-col {
@@ -572,7 +576,8 @@ body, h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
#map-section .branch-text {
color: #111827; /* text-gray-900 */
color: #111827;
/* text-gray-900 */
}
#map-section .branch-card {
@@ -581,12 +586,14 @@ body, h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
/* Active State */
#map-section .branch-card.active {
background-color: var(--color-old-brick); /* bg-primary-600 */
background-color: var(--color-old-brick);
/* bg-primary-600 */
}
#map-section .branch-card.active .branch-title,
#map-section .branch-card.active .branch-text,
#map-section .branch-card.active .branch-icon {
color: #ffffff; /* text-white */
color: #ffffff;
/* text-white */
}
/* Hero Background Override */
@@ -644,4 +651,482 @@ i, .btn-outline-primary {
.bg-linear-service {
background: linear-gradient(#60a7d700, var(--color-old-brick)) !important;
}
}
/* Training Detail Layout */
.training-section {
padding-top: 1.5rem;
padding-bottom: 1.5rem;
}
.training-layout {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.training-main {
flex: 1 1 100%;
min-width: 0;
}
.training-sidebar {
flex: 1 1 100%;
}
.training-sticky {
position: static;
}
@media (min-width: 768px) {
.training-section {
padding-top: 2rem;
padding-bottom: 2rem;
}
.training-layout {
gap: 1.5rem;
}
}
@media (min-width: 1024px) {
.training-container {
padding-left: 2.5rem;
padding-right: 2.5rem;
}
.training-layout {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.training-main {
grid-column: span 9 / span 9;
}
.training-sidebar {
grid-column: span 3 / span 3;
}
.training-sticky {
position: sticky;
top: 132px;
}
}
@media (min-width: 1280px) {
.training-section {
padding-top: 3rem;
padding-bottom: 3rem;
}
.training-layout {
gap: 2rem;
}
}
.education .responsive-flex-container {
max-width: 1280px;
margin: auto;
}
.education .bg-primary-600.rounded-lg.p-4.flex.flex-col.justify-center {
animation-composition: replace;
animation-delay: 0s;
animation-duration: 0s;
animation-iteration-count: 1;
animation-play-state: running;
animation-timing-function: ease;
backface-visibility: visible;
background-attachment: scroll;
background-clip: border-box;
background-color: rgb(13, 113, 186);
background-origin: padding-box;
background-position: 0% 0%;
background-repeat: repeat;
baseline-shift: 0px;
block-size: 388.609px;
border-block-end-color: rgb(0, 0, 0);
border-block-end-style: solid;
border-block-end-width: 0px;
border-block-start-color: rgb(0, 0, 0);
border-block-start-style: solid;
border-block-start-width: 0px;
border-bottom-color: rgb(0, 0, 0);
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
border-bottom-style: solid;
border-bottom-width: 0px;
border-collapse: separate;
border-end-end-radius: 8px;
border-end-start-radius: 8px;
border-image-outset: 0;
border-image-repeat: stretch;
border-image-slice: 100%;
border-image-width: 1;
border-inline-end-color: rgb(0, 0, 0);
border-inline-end-style: solid;
border-inline-end-width: 0px;
border-inline-start-color: rgb(0, 0, 0);
border-inline-start-style: solid;
border-inline-start-width: 0px;
border-left-color: rgb(0, 0, 0);
border-left-style: solid;
border-left-width: 0px;
border-right-color: rgb(0, 0, 0);
border-right-style: solid;
border-right-width: 0px;
border-start-end-radius: 8px;
border-start-start-radius: 8px;
border-top-color: rgb(0, 0, 0);
border-top-left-radius: 8px;
border-top-right-radius: 8px;
border-top-style: solid;
border-top-width: 0px;
box-decoration-break: slice;
box-sizing: border-box;
caption-side: top;
caret-color: rgb(0, 0, 0);
clip-rule: nonzero;
color: rgb(0, 0, 0);
color-interpolation: srgb;
color-interpolation-filters: linearrgb;
column-fill: balance;
column-rule-color: rgb(0, 0, 0);
column-rule-inset-cap-end: 0px;
column-rule-inset-cap-start: 0px;
column-rule-inset-junction-end: 0px;
column-rule-inset-junction-start: 0px;
column-rule-width: 3px;
content-visibility: visible;
corner-bottom-left-shape: round;
corner-bottom-right-shape: round;
corner-end-end-shape: round;
corner-end-start-shape: round;
corner-start-end-shape: round;
corner-start-start-shape: round;
corner-top-left-shape: round;
corner-top-right-shape: round;
cx: 0px;
cy: 0px;
direction: ltr;
display: flex;
dynamic-range-limit: no-limit;
empty-cells: show;
field-sizing: fixed;
fill: rgb(0, 0, 0);
fill-opacity: 1;
fill-rule: nonzero;
flex-direction: column;
flex-grow: 0;
flex-line-count: 1;
flex-shrink: 1;
flex-wrap: nowrap;
flood-color: rgb(0, 0, 0);
flood-opacity: 1;
font-family: 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-stretch: 100%;
font-weight: 400;
grid-auto-flow: row;
height: 388.609px;
hyphens: manual;
image-orientation: from-image;
inline-size: 276px;
inset-block-start: 132px;
interpolate-size: numeric-only;
justify-content: center;
lighting-color: rgb(255, 255, 255);
line-height: 24px;
list-style-position: outside;
list-style-type: disc;
margin-block-end: 0px;
margin-block-start: 0px;
margin-bottom: 0px;
margin-inline-end: 0px;
margin-inline-start: 0px;
margin-left: 0px;
margin-right: 0px;
margin-top: 0px;
mask-clip: border-box;
mask-composite: add;
mask-mode: match-source;
mask-origin: border-box;
mask-position: 0% 0%;
mask-repeat: repeat;
mask-type: luminance;
math-depth: 0;
min-block-size: 0px;
min-height: 0px;
min-inline-size: 0px;
min-width: 0px;
object-fit: fill;
object-position: 50% 50%;
offset-distance: 0px;
offset-rotate: auto 0deg;
opacity: 1;
order: 0;
orphans: 2;
outline-color: rgb(0, 0, 0);
outline-offset: 0px;
outline-width: 3px;
overflow-block: visible;
overflow-clip-margin: 0px;
overflow-inline: visible;
overflow-x: visible;
overflow-y: visible;
padding-block-end: 16px;
padding-block-start: 16px;
padding-bottom: 16px;
padding-inline-end: 16px;
padding-inline-start: 16px;
padding-left: 16px;
padding-right: 16px;
padding-top: 16px;
perspective-origin: 138px 194.297px;
position: sticky;
position-visibility: anchors-visible;
print-color-adjust: economy;
r: 0px;
reading-order: 0;
row-rule-color: rgb(0, 0, 0);
row-rule-inset-cap-end: 0px;
row-rule-inset-cap-start: 0px;
row-rule-inset-junction-end: 0px;
row-rule-inset-junction-start: 0px;
row-rule-width: 3px;
ruby-align: space-around;
ruby-position: over;
rule-overlap: row-over-column;
scroll-margin-block-end: 0px;
scroll-margin-block-start: 0px;
scroll-margin-bottom: 0px;
scroll-margin-inline-end: 0px;
scroll-margin-inline-start: 0px;
scroll-margin-left: 0px;
scroll-margin-right: 0px;
scroll-margin-top: 0px;
scroll-timeline-axis: block;
shape-image-threshold: 0;
shape-margin: 0px;
stop-color: rgb(0, 0, 0);
stop-opacity: 1;
stroke-dashoffset: 0px;
stroke-linecap: butt;
stroke-linejoin: miter;
stroke-miterlimit: 4;
stroke-opacity: 1;
stroke-width: 1px;
tab-size: 4;
text-align: start;
text-anchor: start;
text-autospace: no-autospace;
text-decoration-color: rgb(0, 0, 0);
text-decoration-style: solid;
text-emphasis-color: rgb(0, 0, 0);
text-emphasis-position: over;
text-indent: 0px;
text-orientation: mixed;
text-overflow: clip;
text-size-adjust: 100%;
text-wrap-mode: wrap;
top: 132px;
transform-box: view-box;
transform-origin: 138px 194.305px;
transform-style: flat;
transition-delay: 0s;
transition-duration: 0s;
transition-property: all;
transition-timing-function: ease;
unicode-bidi: isolate;
vertical-align: baseline;
view-timeline-axis: block;
visibility: visible;
white-space-collapse: collapse;
widows: 2;
width: 276px;
word-spacing: 0px;
writing-mode: horizontal-tb;
x: 0px;
y: 0px;
zoom: 1;
/* -webkit-border-horizontal-spacing: 0px;
-webkit-border-vertical-spacing: 0px;
-webkit-box-align: stretch;
-webkit-box-decoration-break: slice;
-webkit-box-flex: 0;
-webkit-box-ordinal-group: 1;
-webkit-box-orient: horizontal;
-webkit-box-pack: start;
-webkit-locale: &quot;vi&quot;;
-webkit-mask-box-image-outset: 0;
-webkit-mask-box-image-repeat: stretch;
-webkit-mask-box-image-slice: 0 fill;
-webkit-mask-position-x: 0%;
-webkit-mask-position-y: 0%;
-webkit-rtl-ordering: logical;
-webkit-ruby-position: before;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-text-fill-color: rgb(0, 0, 0);
-webkit-text-orientation: vertical-right;
-webkit-text-stroke-color: rgb(0, 0, 0);
-webkit-text-stroke-width: 0px;
-webkit-user-modify: read-only;
-webkit-writing-mode: horizontal-tb;
--container-md: 28rem;
--tw-ring-offset-shadow: 0 0 #0000;
--color-blue-100: oklch(93.2% .032 255.585);
--tw-inset-shadow-alpha: 100%;
--tw-ring-shadow: 0 0 #0000;
--text-2xl: 1.5rem;
--aspect-video: 16/9;
--color-green-600: oklch(62.7% .194 149.214);
--leading-relaxed: 1.625;
--blur-lg: 16px;
--color-sky-500: oklch(68.5% .169 237.323);
--text-9xl: 8rem;
--tw-drop-shadow-alpha: 100%;
--font-weight-black: 900;
--text-lg: 1.125rem;
--color-neutral-600: oklch(43.9% 0 0);
--default-transition-timing-function: cubic-bezier(.4,0,.2,1);
--color-blue-700: oklch(48.8% .243 264.376);
--color-green-50: oklch(98.2% .018 155.826);
--tw-outline-style: solid;
--text-2xl--line-height: calc(2/1.5);
--color-green-100: oklch(96.2% .044 156.743);
--tw-inset-ring-shadow: 0 0 #0000;
--text-base--line-height: calc(1.5/1);
--spacing: .25rem;
--tw-border-spacing-x: 0px;
--color-red-200: oklch(88.5% .062 18.334);
--color-neutral-200: oklch(92.2% 0 0);
--tw-translate-z: 0;
--tw-gradient-via: rgba(0, 0, 0, 0);
--tw-scale-y: 1;
--font-mono: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Liberation Mono&quot;,&quot;Courier New&quot;,monospace;
--container-6xl: 72rem;
--text-xl--line-height: calc(1.75/1.25);
--color-green-800: oklch(44.8% .119 151.328);
--font-weight-semibold: 600;
--container-3xl: 48rem;
--color-red-700: oklch(50.5% .213 27.518);
--text-sm: .875rem;
--tw-translate-y: 0;
--ease-out: cubic-bezier(0,0,.2,1);
--swiper-theme-color: #007aff;
--color-blue-500: oklch(62.3% .214 259.815);
--color-green-500: oklch(72.3% .219 149.579);
--animate-spin: spin 1s linear infinite;
--container-4xl: 56rem;
--text-9xl--line-height: 1;
--tw-inset-shadow: 0 0 #0000;
--tw-ring-offset-color: #fff;
--text-lg--line-height: calc(1.75/1.125);
--tw-divide-x-reverse: 0;
--tw-gradient-from: rgba(0, 0, 0, 0);
--tw-content: &quot;&quot;;
--color-white: #fff;
--color-blue-400: oklch(70.7% .165 254.624);
--tw-shadow-alpha: 100%;
--tw-gradient-to: rgba(0, 0, 0, 0);
--leading-tight: 1.25;
--radius-sm: .25rem;
--color-blue-600: oklch(54.6% .245 262.881);
--font-sans: 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;;
--text-4xl: 2.25rem;
--color-green-200: oklch(92.5% .084 155.995);
--tw-scale-z: 1;
--tw-border-style: solid;
--font-weight-bold: 700;
--swiper-navigation-size: 44px;
--color-red-800: oklch(44.4% .177 26.899);
--color-red-600: oklch(57.7% .245 27.325);
--container-2xl: 42rem;
--color-red-50: oklch(97.1% .013 17.38);
--tw-shadow: 0 0 #0000;
--text-sm--line-height: calc(1.25/.875);
--tw-translate-x: 0;
--tw-ring-offset-width: 0px;
--container-sm: 24rem;
--color-blue-200: oklch(88.2% .059 254.128);
--color-blue-50: oklch(97% .014 254.604);
--color-red-500: oklch(63.7% .237 25.331);
--tw-gradient-via-position: 50%;
--text-3xl--line-height: calc(2.25/1.875);
--text-6xl: 3.75rem;
--text-3xl: 1.875rem;
--color-black: #000;
--text-xs--line-height: calc(1/.75);
--default-font-family: 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-weight-light: 300;
--tw-space-x-reverse: 0;
--font-weight-normal: 400;
--tw-scale-x: 1;
--color-neutral-700: oklch(37.1% 0 0);
--text-xs: .75rem;
--font-weight-medium: 500;
--tw-gradient-to-position: 100%;
--tw-border-spacing-y: 0px;
--color-neutral-50: oklch(98.5% 0 0);
--default-transition-duration: .15s;
--radius-xl: .75rem;
--animate-pulse: pulse 2s cubic-bezier(.4,0,.6,1)infinite;
--radius-2xl: 1rem;
--tw-space-y-reverse: 0;
--text-6xl--line-height: 1;
--container-xl: 36rem;
--color-blue-800: oklch(42.4% .199 265.638);
--color-sky-700: oklch(50% .134 242.749);
--radius-lg: .5rem;
--text-4xl--line-height: calc(2.5/2.25);
--text-xl: 1.25rem;
--color-orange-300: oklch(83.7% .128 66.29);
--radius-md: .375rem;
--container-7xl: 80rem;
--text-base: 1rem;
--tw-gradient-from-position: 0px;
--default-mono-font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,&quot;Liberation Mono&quot;,&quot;Courier New&quot;,monospace;
--ease-in-out: cubic-bezier(.4,0,.2,1); */
}
/* CSS cho Tab Đa năng - Giao diện Mới (Segmented Control) */
.custom-tabs-nav-container {
display: flex;
flex-direction: column;
justify-content: center;
margin-bottom: 2rem;
width: 100%;
}
.tabs-container {
background-color: var(--color-old-brick); /* primary-600 */
border-radius: 8px; /* rounded-lg */
padding: 4px; /* p-1 */
display: flex;
gap: 4px; /* gap-x-1 */
width: 100%;
max-width: 860px;
}
@media (max-width: 768px) {
.tabs-container {
flex-direction: column;
width: 100%;
max-width: 100%;
}
}
.custom-tab-btn {
padding: 8px 16px; /* px-4 py-2 */
border: none;
background-color: transparent;
color: white;
cursor: pointer;
border-radius: 6px; /* rounded-md */
transition: all 0.2s ease-in-out;
font-weight: 600;
font-size: 16px;
flex: 1;
text-align: center;
min-width: 120px;
}
.custom-tab-btn:hover {
color: rgba(255, 255, 255, 0.8);
background-color: transparent;
}
.custom-tab-btn.active {
background-color: white !important;
color: var(--color-old-brick) !important; /* text-primary-600 */
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
@@ -37,10 +37,10 @@ window.SISEditorPlugins = window.SISEditorPlugins || {};
* @param {string} holderId - The DOM element ID for the editor container
* @param {string} hiddenInputId - The DOM element ID for the hidden input storing JSON
* @param {object|null} initialData - Pre-existing Editor.js JSON data to load
* @param {boolean} [skipSubmitHandler=false] - If true, prevents automatic form submission handling
* @returns {EditorJS} The editor instance
*/
function initSISEditor(holderId, hiddenInputId, initialData) {
'use strict';
function initSISEditor(holderId, hiddenInputId, initialData, skipSubmitHandler = false) {
// === Built-in Tools ===
var builtInTools = {
@@ -158,7 +158,7 @@ function initSISEditor(holderId, hiddenInputId, initialData) {
// === Form submission handler ===
// Ensure the latest content is saved before form submit
var form = document.querySelector('form');
if (form) {
if (form && !skipSubmitHandler) {
var submitHandler = function(event) {
event.preventDefault();
editor.save().then(function(outputData) {
@@ -0,0 +1,31 @@
class TabEndTool {
static get toolbox() {
return {
title: 'Kết thúc Tabs',
icon: '<svg viewBox="0 0 24 24" width="20" height="20"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>'
};
}
constructor({ data, api }) {
this.data = data;
this.api = api;
}
render() {
this.wrapper = document.createElement('div');
this.wrapper.style.padding = '10px';
this.wrapper.style.border = '2px dashed #dc3545';
this.wrapper.style.backgroundColor = '#fff3f3';
this.wrapper.style.margin = '20px 0';
this.wrapper.style.textAlign = 'center';
this.wrapper.style.fontWeight = 'bold';
this.wrapper.style.color = '#dc3545';
this.wrapper.innerHTML = '--- KẾT THÚC CỤM TABS ---';
return this.wrapper;
}
save(blockContent) {
return {};
}
}
@@ -0,0 +1,65 @@
/**
* Tab Split Block Plugin for Editor.js
*
* Allows users to insert a visual "Tab Split" marker in the editor.
* When saved, it outputs `{ type: 'tabSplit', data: {} }`.
* The frontend parser will use this block to split content into tabs.
*/
class TabSplitTool {
/**
* Notify Editor.js that this tool doesn't require any inline toolbar
*/
static get toolbox() {
return {
title: 'Tab Split',
icon: '<svg width="17" height="15" viewBox="0 0 336 276" xmlns="http://www.w3.org/2000/svg"><path d="M291 150V79c0-19-15-34-34-34H79c-19 0-34 15-34 34v42l67-44 81 72 56-29 42 30zm0 52l-43-30-56 30-81-67-66 39v23c0 19 15 34 34 34h178c17 0 31-13 34-29zM79 0h178c44 0 79 35 79 79v118c0 44-35 79-79 79H79c-44 0-79-35-79-79V79C0 35 35 0 79 0z"/></svg>'
};
}
/**
* Allow rendering without data (when inserted from the toolbox)
*/
static get isReadOnlySupported() {
return true;
}
constructor({ data, config, api }) {
this.api = api;
this.data = data || {};
this.wrapper = undefined;
}
/**
* Render the UI for this block
* @returns {HTMLElement}
*/
render() {
this.wrapper = document.createElement('div');
this.wrapper.classList.add('ce-tab-split');
this.wrapper.style.textAlign = 'center';
this.wrapper.style.padding = '15px 0';
this.wrapper.style.margin = '10px 0';
this.wrapper.style.borderTop = '2px dashed #4e73df';
this.wrapper.style.borderBottom = '2px dashed #4e73df';
this.wrapper.style.backgroundColor = '#f8f9fc';
this.wrapper.style.color = '#4e73df';
this.wrapper.style.fontWeight = 'bold';
this.wrapper.style.fontSize = '14px';
this.wrapper.style.textTransform = 'uppercase';
this.wrapper.style.letterSpacing = '2px';
this.wrapper.style.userSelect = 'none';
this.wrapper.innerHTML = '<span><i class="fas fa-columns mr-2"></i> --- TAB SPLIT --- </span>';
return this.wrapper;
}
/**
* Save data from this block
* @param {HTMLElement} blockContent
* @returns {Object}
*/
save(blockContent) {
return {}; // We only care about the block type, no inner data needed
}
}
@@ -0,0 +1,50 @@
class TabStartTool {
static get toolbox() {
return {
title: 'Bắt đầu Tab',
icon: '<svg viewBox="0 0 24 24" width="20" height="20"><path d="M3 3h18v18H3V3zm16 16V7H5v12h14zM7 9h10v2H7V9z"/></svg>'
};
}
constructor({ data, api }) {
this.data = {
title: data.title || ''
};
this.api = api;
}
render() {
this.wrapper = document.createElement('div');
this.wrapper.style.padding = '15px';
this.wrapper.style.border = '2px dashed #007bff';
this.wrapper.style.backgroundColor = '#f8f9fa';
this.wrapper.style.margin = '20px 0';
this.wrapper.style.borderRadius = '5px';
const label = document.createElement('div');
label.innerHTML = '<span style="color:#007bff;font-weight:bold;margin-right:10px;">[ THẺ TAB MỚI ]</span> Nhập tên thẻ Tab (Ví dụ: Tổng quan, Lịch học, Học phí...):';
label.style.marginBottom = '10px';
const input = document.createElement('input');
input.type = 'text';
input.value = this.data.title;
input.placeholder = 'Tên Tab...';
input.classList.add('form-control');
input.addEventListener('input', (event) => {
this.data.title = event.target.value;
});
this.wrapper.appendChild(label);
this.wrapper.appendChild(input);
return this.wrapper;
}
save(blockContent) {
const input = blockContent.querySelector('input');
return {
title: input ? input.value : this.data.title
};
}
}
File diff suppressed because one or more lines are too long
@@ -109,6 +109,45 @@
</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">
<div class="lg:container max-lg:pl-5 max-md:px-0" th:if="${daoTaoPosts != null and not #lists.isEmpty(daoTaoPosts)}">
<div class="flex justify-between items-end md:mb-6 mb-4 lg:mb-8 max-lg:pr-5 max-md:px-4">
<div>
<h2 class="display-7 text-primary-600">Khóa học Đào tạo</h2>
</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="">
<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="">
<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>
</div>
</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">
<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">
</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>
</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">
<div>
@@ -644,6 +683,11 @@
prev: '.btn-navigation-gallery-prev',
next: '.btn-navigation-gallery-next',
pagination: '.swiper-pagination-training-gallery'
}, {
slug: 'khoa-hoc',
prev: '.btn-navigation-khoa-hoc-prev',
next: '.btn-navigation-khoa-hoc-next',
pagination: ''
}, {
slug: 'chung-chi',
prev: '.btn-navigation-cap-chung-chi-prev',
@@ -268,9 +268,16 @@
<!-- Init Editor -->
<script th:src="@{/js/manage/editor-config.js}"></script>
<!-- Initialize the editor with existing content (if editing) -->
<!-- Initialize the editor and page-specific scripts -->
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function () {
// Register RawTool for imported HTML content
window.SISEditorPlugins = window.SISEditorPlugins || {};
if (typeof RawTool !== 'undefined') {
window.SISEditorPlugins['raw'] = { class: RawTool };
}
// --- Editor.js Initialization ---
var existingContent = document.getElementById('editorContent').value;
var initialData = null;
@@ -278,7 +285,18 @@
try {
initialData = JSON.parse(existingContent);
} catch (e) {
console.warn('[SIS Editor] Existing content is not valid JSON, starting fresh.');
console.warn('[SIS Editor] Existing content is not valid JSON, treating as Raw HTML.');
initialData = {
time: Date.now(),
blocks: [
{
type: "raw",
data: {
html: existingContent
}
}
]
};
}
}
@@ -260,8 +260,10 @@
<option th:each="s : ${statuses}" th:value="${s}" th:text="${s}"></option>
</select>
</div>
<div class="form-group mt-3">
<label for="postLayout" class="font-weight-bold">Layout</label>
<div class="form-group mb-0">
<label for="postLayout" class="font-weight-bold">
<i class="fas fa-columns"></i> Layout
</label>
<select class="form-control" id="postLayout" th:field="*{layout}">
<option th:each="l : ${layouts}" th:value="${l}" th:text="${l}"></option>
</select>
@@ -382,9 +384,14 @@
<script src="https://cdn.jsdelivr.net/npm/@editorjs/underline@1.1.0/dist/bundle.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/image@2.9.0/dist/image.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/attaches@1.3.0/dist/bundle.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/raw@2.4.0/dist/bundle.js"></script>
<!-- Custom SIS Editor Plugins -->
<script th:src="@{/js/manage/editor-plugins/html-snippet.js}"></script>
<script th:src="@{/js/manage/editor-plugins/tab-split.js}"></script>
<script th:src="@{/js/manage/editor-plugins/training-course.js}"></script>
<script th:src="@{/js/manage/editor-plugins/tab-start.js}"></script>
<script th:src="@{/js/manage/editor-plugins/tab-end.js}"></script>
<!-- Init Editor -->
<script th:src="@{/js/manage/editor-config.js}"></script>
@@ -392,6 +399,24 @@
<!-- Initialize the editor and page-specific scripts -->
<script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function () {
// Register Plugins
window.SISEditorPlugins = window.SISEditorPlugins || {};
if (typeof RawTool !== 'undefined') {
window.SISEditorPlugins['raw'] = { class: RawTool };
}
if (typeof TabSplitTool !== 'undefined') {
window.SISEditorPlugins['tabSplit'] = { class: TabSplitTool };
}
if (typeof TrainingCourseInfoTool !== 'undefined') {
}
if (typeof TabStartTool !== 'undefined') {
window.SISEditorPlugins['tabStart'] = { class: TabStartTool };
}
if (typeof TabEndTool !== 'undefined') {
window.SISEditorPlugins['tabEnd'] = { class: TabEndTool };
}
// --- Editor.js Initialization ---
var existingContent = document.getElementById('editorContent').value;
var initialData = null;
@@ -400,7 +425,18 @@
try {
initialData = JSON.parse(existingContent);
} catch (e) {
console.warn('[SIS Editor] Existing content is not valid JSON, starting fresh.');
console.warn('[SIS Editor] Existing content is not valid JSON, treating as Raw HTML.');
initialData = {
time: Date.now(),
blocks: [
{
type: "raw",
data: {
html: existingContent
}
}
]
};
}
}
@@ -490,7 +526,7 @@
var eventTimeGroup = document.getElementById('eventTimeGroup');
var eventLocationGroup = document.getElementById('eventLocationGroup');
function toggleEventTime() {
if (layoutSelect.value === 'EVENT') {
if (layoutSelect.value === 'EVENT' || layoutSelect.value === 'TRAINING') {
eventTimeGroup.style.display = 'block';
if (eventLocationGroup) eventLocationGroup.style.display = 'block';
} else {
@@ -58,8 +58,14 @@
var data = JSON.parse(rawData);
var html = "";
if (data.blocks) {
data.blocks.forEach(function(block) {
switch(block.type) {
var inTabs = false;
var tabsGroup = null;
function renderSingleBlock(block) {
var html = "";
switch(block.type) {
case "header":
html += "<h" + block.data.level + ">" + block.data.text + "</h" + block.data.level + ">";
break;
@@ -80,10 +86,115 @@
case "quote":
html += "<blockquote>" + block.data.text + " <cite>" + block.data.caption + "</cite></blockquote>";
break;
default:
console.log("Unknown block type", block.type);
}
});
case "raw":
html += block.data.html;
break;
case "tabStart":
case "tabEnd":
break;
default:
console.log("Unknown block type", block.type);
}
return html;
}
var blockList = data && data.blocks ? data.blocks : (typeof blocks !== 'undefined' ? blocks : []);
var finalHtml = "";
blockList.forEach(function(block) {
if (block.type === 'tabStart') {
if (!inTabs) {
inTabs = true;
tabsGroup = { tabs: [] };
}
tabsGroup.tabs.push({
title: block.data.title || 'Tab',
contentHtml: ''
});
return;
}
if (block.type === 'tabEnd') {
if (inTabs && tabsGroup) {
inTabs = false;
var groupHtml = '<div class="custom-tabs-wrapper">';
groupHtml += '<div class="custom-tabs-nav-container"><div class="tabs-container">';
tabsGroup.tabs.forEach(function(tab, index) {
var activeClass = (index === 0) ? 'active' : '';
groupHtml += '<button class="dao-tao-tab-btn custom-tab-btn ' + activeClass + '" data-tab-idx="' + index + '">' + tab.title + '</button>';
});
groupHtml += '</div>';
tabsGroup.tabs.forEach(function(tab, index) {
var displayStyle = (index === 0) ? 'block' : 'none';
groupHtml += '<div class="dao-tao-tab-panel custom-tab-panel" data-tab-idx="' + index + '" style="display: ' + displayStyle + ';">';
groupHtml += tab.contentHtml;
groupHtml += '</div>';
});
groupHtml += '</div>';
finalHtml += groupHtml;
tabsGroup = null;
}
return;
}
var blockHtml = renderSingleBlock(block);
if (inTabs && tabsGroup && tabsGroup.tabs.length > 0) {
tabsGroup.tabs[tabsGroup.tabs.length - 1].contentHtml += blockHtml;
} else {
finalHtml += blockHtml;
}
});
if (inTabs && tabsGroup) {
// Close unclosed tabs at the end
var groupHtml = '<div class="custom-tabs-wrapper">';
groupHtml += '<div class="custom-tabs-nav-container"><div class="tabs-container">';
tabsGroup.tabs.forEach(function(tab, index) {
var activeClass = (index === 0) ? 'active' : '';
groupHtml += '<button class="dao-tao-tab-btn custom-tab-btn ' + activeClass + '" data-tab-idx="' + index + '">' + tab.title + '</button>';
});
groupHtml += '</div>';
tabsGroup.tabs.forEach(function(tab, index) {
var displayStyle = (index === 0) ? 'block' : 'none';
groupHtml += '<div class="dao-tao-tab-panel custom-tab-panel" data-tab-idx="' + index + '" style="display: ' + displayStyle + ';">';
groupHtml += tab.contentHtml;
groupHtml += '</div>';
});
groupHtml += '</div>';
finalHtml += groupHtml;
}
html = finalHtml;
document.addEventListener('click', function(e) {
if (e.target.classList.contains('custom-tab-btn')) {
var btn = e.target;
var container = btn.closest('.custom-tabs-wrapper');
if (!container) return;
var idx = btn.getAttribute('data-tab-idx');
container.querySelectorAll('.custom-tab-btn').forEach(function(b) {
b.classList.remove('active');
});
btn.classList.add('active');
container.querySelectorAll('.custom-tab-panel').forEach(function(p) {
p.style.display = 'none';
});
var activePanel = container.querySelector('.custom-tab-panel[data-tab-idx="' + idx + '"]');
if (activePanel) activePanel.style.display = 'block';
}
});
// Assign back to whatever variable the original script used.
}
parsedContainer.innerHTML = html;
} catch(e) {
@@ -111,8 +111,14 @@
var data = JSON.parse(rawData);
var html = "";
if (data.blocks) {
data.blocks.forEach(function(block) {
switch(block.type) {
var inTabs = false;
var tabsGroup = null;
function renderSingleBlock(block) {
var html = "";
switch(block.type) {
case "paragraph":
html += "<p>" + block.data.text + "</p>";
break;
@@ -130,6 +136,117 @@
case "image":
html += "<figure><img src='" + block.data.file.url + "' alt='" + (block.data.caption || "") + "'><figcaption>" + (block.data.caption || "") + "</figcaption></figure>";
break;
case "tabStart":
case "tabEnd":
break;
default:
console.log("Unknown block type", block.type);
}
return html;
}
var blockList = data && data.blocks ? data.blocks : (typeof blocks !== 'undefined' ? blocks : []);
var finalHtml = "";
blockList.forEach(function(block) {
if (block.type === 'tabStart') {
if (!inTabs) {
inTabs = true;
tabsGroup = { tabs: [] };
}
tabsGroup.tabs.push({
title: block.data.title || 'Tab',
contentHtml: ''
});
return;
}
if (block.type === 'tabEnd') {
if (inTabs && tabsGroup) {
inTabs = false;
var groupHtml = '<div class="custom-tabs-wrapper">';
groupHtml += '<div class="custom-tabs-nav-container"><div class="tabs-container">';
tabsGroup.tabs.forEach(function(tab, index) {
var activeClass = (index === 0) ? 'active' : '';
groupHtml += '<button class="dao-tao-tab-btn custom-tab-btn ' + activeClass + '" data-tab-idx="' + index + '">' + tab.title + '</button>';
});
groupHtml += '</div>';
tabsGroup.tabs.forEach(function(tab, index) {
var displayStyle = (index === 0) ? 'block' : 'none';
groupHtml += '<div class="dao-tao-tab-panel custom-tab-panel" data-tab-idx="' + index + '" style="display: ' + displayStyle + ';">';
groupHtml += tab.contentHtml;
groupHtml += '</div>';
});
groupHtml += '</div>';
finalHtml += groupHtml;
tabsGroup = null;
}
return;
}
var blockHtml = renderSingleBlock(block);
if (inTabs && tabsGroup && tabsGroup.tabs.length > 0) {
tabsGroup.tabs[tabsGroup.tabs.length - 1].contentHtml += blockHtml;
} else {
finalHtml += blockHtml;
}
});
if (inTabs && tabsGroup) {
// Close unclosed tabs at the end
var groupHtml = '<div class="custom-tabs-wrapper">';
groupHtml += '<div class="custom-tabs-nav-container"><div class="tabs-container">';
tabsGroup.tabs.forEach(function(tab, index) {
var activeClass = (index === 0) ? 'active' : '';
groupHtml += '<button class="dao-tao-tab-btn custom-tab-btn ' + activeClass + '" data-tab-idx="' + index + '">' + tab.title + '</button>';
});
groupHtml += '</div>';
tabsGroup.tabs.forEach(function(tab, index) {
var displayStyle = (index === 0) ? 'block' : 'none';
groupHtml += '<div class="dao-tao-tab-panel custom-tab-panel" data-tab-idx="' + index + '" style="display: ' + displayStyle + ';">';
groupHtml += tab.contentHtml;
groupHtml += '</div>';
});
groupHtml += '</div>';
finalHtml += groupHtml;
}
html = finalHtml;
document.addEventListener('click', function(e) {
if (e.target.classList.contains('custom-tab-btn')) {
var btn = e.target;
var container = btn.closest('.custom-tabs-wrapper');
if (!container) return;
var idx = btn.getAttribute('data-tab-idx');
container.querySelectorAll('.custom-tab-btn').forEach(function(b) {
b.classList.remove('active');
});
btn.classList.add('active');
container.querySelectorAll('.custom-tab-panel').forEach(function(p) {
p.style.display = 'none';
});
var activePanel = container.querySelector('.custom-tab-panel[data-tab-idx="' + idx + '"]');
if (activePanel) activePanel.style.display = 'block';
}
});
// Assign back to whatever variable the original script used.
html += "</" + tag + ">";
break;
case "image":
html += "<figure><img src='" + block.data.file.url + "' alt='" + (block.data.caption || "") + "'><figcaption>" + (block.data.caption || "") + "</figcaption></figure>";
break;
default:
break;
}
@@ -104,8 +104,14 @@
var data = JSON.parse(rawData);
var html = "";
if (data.blocks) {
data.blocks.forEach(function(block) {
switch(block.type) {
var inTabs = false;
var tabsGroup = null;
function renderSingleBlock(block) {
var html = "";
switch(block.type) {
case "header":
html += "<h" + block.data.level + ">" + block.data.text + "</h" + block.data.level + ">";
break;
@@ -126,10 +132,115 @@
case "quote":
html += "<blockquote>" + block.data.text + " <cite>" + block.data.caption + "</cite></blockquote>";
break;
default:
console.log("Unknown block type", block.type);
}
});
case "raw":
html += block.data.html;
break;
case "tabStart":
case "tabEnd":
break;
default:
console.log("Unknown block type", block.type);
}
return html;
}
var blockList = data && data.blocks ? data.blocks : (typeof blocks !== 'undefined' ? blocks : []);
var finalHtml = "";
blockList.forEach(function(block) {
if (block.type === 'tabStart') {
if (!inTabs) {
inTabs = true;
tabsGroup = { tabs: [] };
}
tabsGroup.tabs.push({
title: block.data.title || 'Tab',
contentHtml: ''
});
return;
}
if (block.type === 'tabEnd') {
if (inTabs && tabsGroup) {
inTabs = false;
var groupHtml = '<div class="custom-tabs-wrapper">';
groupHtml += '<div class="custom-tabs-nav-container"><div class="tabs-container">';
tabsGroup.tabs.forEach(function(tab, index) {
var activeClass = (index === 0) ? 'active' : '';
groupHtml += '<button class="dao-tao-tab-btn custom-tab-btn ' + activeClass + '" data-tab-idx="' + index + '">' + tab.title + '</button>';
});
groupHtml += '</div>';
tabsGroup.tabs.forEach(function(tab, index) {
var displayStyle = (index === 0) ? 'block' : 'none';
groupHtml += '<div class="dao-tao-tab-panel custom-tab-panel" data-tab-idx="' + index + '" style="display: ' + displayStyle + ';">';
groupHtml += tab.contentHtml;
groupHtml += '</div>';
});
groupHtml += '</div>';
finalHtml += groupHtml;
tabsGroup = null;
}
return;
}
var blockHtml = renderSingleBlock(block);
if (inTabs && tabsGroup && tabsGroup.tabs.length > 0) {
tabsGroup.tabs[tabsGroup.tabs.length - 1].contentHtml += blockHtml;
} else {
finalHtml += blockHtml;
}
});
if (inTabs && tabsGroup) {
// Close unclosed tabs at the end
var groupHtml = '<div class="custom-tabs-wrapper">';
groupHtml += '<div class="custom-tabs-nav-container"><div class="tabs-container">';
tabsGroup.tabs.forEach(function(tab, index) {
var activeClass = (index === 0) ? 'active' : '';
groupHtml += '<button class="dao-tao-tab-btn custom-tab-btn ' + activeClass + '" data-tab-idx="' + index + '">' + tab.title + '</button>';
});
groupHtml += '</div>';
tabsGroup.tabs.forEach(function(tab, index) {
var displayStyle = (index === 0) ? 'block' : 'none';
groupHtml += '<div class="dao-tao-tab-panel custom-tab-panel" data-tab-idx="' + index + '" style="display: ' + displayStyle + ';">';
groupHtml += tab.contentHtml;
groupHtml += '</div>';
});
groupHtml += '</div>';
finalHtml += groupHtml;
}
html = finalHtml;
document.addEventListener('click', function(e) {
if (e.target.classList.contains('custom-tab-btn')) {
var btn = e.target;
var container = btn.closest('.custom-tabs-wrapper');
if (!container) return;
var idx = btn.getAttribute('data-tab-idx');
container.querySelectorAll('.custom-tab-btn').forEach(function(b) {
b.classList.remove('active');
});
btn.classList.add('active');
container.querySelectorAll('.custom-tab-panel').forEach(function(p) {
p.style.display = 'none';
});
var activePanel = container.querySelector('.custom-tab-panel[data-tab-idx="' + idx + '"]');
if (activePanel) activePanel.style.display = 'block';
}
});
// Assign back to whatever variable the original script used.
}
parsedContainer.innerHTML = html;
} catch(e) {
@@ -177,8 +177,14 @@
var data = JSON.parse(rawData);
var html = "";
if (data.blocks) {
data.blocks.forEach(function(block) {
switch(block.type) {
var inTabs = false;
var tabsGroup = null;
function renderSingleBlock(block) {
var html = "";
switch(block.type) {
case "header":
html += "<h" + block.data.level + ">" + block.data.text + "</h" + block.data.level + ">";
break;
@@ -199,10 +205,115 @@
case "quote":
html += "<blockquote>" + block.data.text + " <cite>" + block.data.caption + "</cite></blockquote>";
break;
default:
console.log("Unknown block type", block.type);
}
});
case "raw":
html += block.data.html;
break;
case "tabStart":
case "tabEnd":
break;
default:
console.log("Unknown block type", block.type);
}
return html;
}
var blockList = data && data.blocks ? data.blocks : (typeof blocks !== 'undefined' ? blocks : []);
var finalHtml = "";
blockList.forEach(function(block) {
if (block.type === 'tabStart') {
if (!inTabs) {
inTabs = true;
tabsGroup = { tabs: [] };
}
tabsGroup.tabs.push({
title: block.data.title || 'Tab',
contentHtml: ''
});
return;
}
if (block.type === 'tabEnd') {
if (inTabs && tabsGroup) {
inTabs = false;
var groupHtml = '<div class="custom-tabs-wrapper">';
groupHtml += '<div class="custom-tabs-nav-container"><div class="tabs-container">';
tabsGroup.tabs.forEach(function(tab, index) {
var activeClass = (index === 0) ? 'active' : '';
groupHtml += '<button class="dao-tao-tab-btn custom-tab-btn ' + activeClass + '" data-tab-idx="' + index + '">' + tab.title + '</button>';
});
groupHtml += '</div>';
tabsGroup.tabs.forEach(function(tab, index) {
var displayStyle = (index === 0) ? 'block' : 'none';
groupHtml += '<div class="dao-tao-tab-panel custom-tab-panel" data-tab-idx="' + index + '" style="display: ' + displayStyle + ';">';
groupHtml += tab.contentHtml;
groupHtml += '</div>';
});
groupHtml += '</div>';
finalHtml += groupHtml;
tabsGroup = null;
}
return;
}
var blockHtml = renderSingleBlock(block);
if (inTabs && tabsGroup && tabsGroup.tabs.length > 0) {
tabsGroup.tabs[tabsGroup.tabs.length - 1].contentHtml += blockHtml;
} else {
finalHtml += blockHtml;
}
});
if (inTabs && tabsGroup) {
// Close unclosed tabs at the end
var groupHtml = '<div class="custom-tabs-wrapper">';
groupHtml += '<div class="custom-tabs-nav-container"><div class="tabs-container">';
tabsGroup.tabs.forEach(function(tab, index) {
var activeClass = (index === 0) ? 'active' : '';
groupHtml += '<button class="dao-tao-tab-btn custom-tab-btn ' + activeClass + '" data-tab-idx="' + index + '">' + tab.title + '</button>';
});
groupHtml += '</div>';
tabsGroup.tabs.forEach(function(tab, index) {
var displayStyle = (index === 0) ? 'block' : 'none';
groupHtml += '<div class="dao-tao-tab-panel custom-tab-panel" data-tab-idx="' + index + '" style="display: ' + displayStyle + ';">';
groupHtml += tab.contentHtml;
groupHtml += '</div>';
});
groupHtml += '</div>';
finalHtml += groupHtml;
}
html = finalHtml;
document.addEventListener('click', function(e) {
if (e.target.classList.contains('custom-tab-btn')) {
var btn = e.target;
var container = btn.closest('.custom-tabs-wrapper');
if (!container) return;
var idx = btn.getAttribute('data-tab-idx');
container.querySelectorAll('.custom-tab-btn').forEach(function(b) {
b.classList.remove('active');
});
btn.classList.add('active');
container.querySelectorAll('.custom-tab-panel').forEach(function(p) {
p.style.display = 'none';
});
var activePanel = container.querySelector('.custom-tab-panel[data-tab-idx="' + idx + '"]');
if (activePanel) activePanel.style.display = 'block';
}
});
// Assign back to whatever variable the original script used.
}
parsedContainer.innerHTML = html;
} catch(e) {
@@ -0,0 +1,92 @@
package com.sisvietnamvn.web;
import com.sisvietnamvn.web.domain.Category;
import com.sisvietnamvn.web.domain.PageStatus;
import com.sisvietnamvn.web.domain.Post;
import com.sisvietnamvn.web.domain.PostLayout;
import com.sisvietnamvn.web.repository.CategoryRepository;
import com.sisvietnamvn.web.repository.PostRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.Optional;
@SpringBootTest(classes = SisvietnamvnApp.class)
public class SeedCourseTest {
@Autowired
private CategoryRepository categoryRepository;
@Autowired
private PostRepository postRepository;
@Test
public void seedNoiSoiCourse() throws IOException {
System.out.println("=========================================");
System.out.println("STARTING SEED COURSE OPERATION...");
System.out.println("=========================================");
// 1. Find or create the Category 'dao-tao'
String categorySlug = "dao-tao";
Category category = categoryRepository.findBySlug(categorySlug).orElseGet(() -> {
Category cat = new Category();
// Generate id dynamically
Long nextId = categoryRepository.count() + 1;
cat.setId(nextId);
cat.setName("Đào tạo");
cat.setSlug(categorySlug);
cat.setDescription("Chương trình đào tạo y khoa");
cat.setCreatedBy("system");
cat.setCreatedDate(Instant.now());
cat = categoryRepository.save(cat);
System.out.println("Created new Category 'dao-tao' with ID: " + cat.getId());
return cat;
});
// 2. Read content from parsed files
String content = Files.readString(Paths.get("/tmp/course_content.html"));
String excerpt = Files.readString(Paths.get("/tmp/course_excerpt.html"));
// 3. Find or create the Post
String postSlug = "chuong-trinh-chung-chi-dao-tao-ky-thuat-chuyen-mon-noi-soi-da-day-noi-soi-dai-trang-va-noi-soi-dieu-tri-co-ban-khoa-02";
Optional<Post> postOpt = postRepository.findBySlug(postSlug);
Post post;
if (postOpt.isPresent()) {
post = postOpt.get();
System.out.println("Found existing Post with ID: " + post.getId() + ", updating...");
} else {
post = new Post();
// Generate id dynamically
Long nextId = postRepository.count() + 1000L;
post.setId(nextId);
post.setSlug(postSlug);
post.setCreatedBy("system");
post.setCreatedDate(Instant.now());
System.out.println("Creating new Post with ID: " + post.getId());
}
post.setTitle("CHƯƠNG TRÌNH CHỨNG CHỈ ĐÀO TẠO KỸ THUẬT CHUYÊN MÔN: NỘI SOI DẠ DÀY, NỘI SOI ĐẠI TRÀNG VÀ NỘI SOI ĐIỀU TRỊ CƠ BẢN, KHÓA 02");
post.setContent(content);
post.setExcerpt(excerpt);
post.setLocation("Giảng đường 3A, lầu 3, khu A, Bệnh viện Đại học Y Dược Thành phố Hồ Chí Minh (215 Hồng Bàng, Phường Chợ Lớn, Thành phố Hồ Chí Minh)");
post.setMetaDescription("38.000.000 VNĐ");
post.setEventTime(Instant.parse("2026-07-27T07:30:00Z"));
post.setStatus(PageStatus.PUBLISHED);
post.setLayout(PostLayout.TRAINING);
post.setCategory(category);
post.setFeaturedImage("https://console.bvdaihoc.com.vn/uploads/KHDT/T%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026/07/N%E1%BB%98I-SOI-D%E1%BA%A0-D%C3%80Y,-N%E1%BB%98I-SOI-%C4%90%E1%BA%A0I-TR%C3%80NG-V%C3%80-N%E1%BB%98I-SOI-%C4%90I%E1%BB%80U-TR%E1%BB%8A-C%C6%A0-B%E1%BA%A2N-(1).gif");
postRepository.save(post);
System.out.println("=========================================");
System.out.println("SEED COURSE COMPLETED SUCCESSFULLY!");
System.out.println("URL path: /dao-tao/" + postSlug);
System.out.println("=========================================");
}
}
File diff suppressed because one or more lines are too long
+4
View File
@@ -0,0 +1,4 @@
import sqlite3
import sys
# find the db file, usually it's application-dev.yml config, or sisvietnamvn.mv.db? It's H2 or Postgres?
File diff suppressed because it is too large Load Diff
+818
View File
@@ -0,0 +1,818 @@
<main class="">
<section class="overflow-hidden relative w-full bg-white xl:h-[580px] md:h-[450px] h-[250px] max-h-[calc(100vh-122px)]">
<div class="absolute inset-0">
<img alt="banner (1)" class="max-md:hidden" data-nimg="fill" decoding="async" fetchpriority="high" loading="eager" sizes="100vw" src="./Đào tạo tại UMC_files/banner%20(1).jpeg" srcset="/_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fbanner%20(1).jpg&amp;w=640&amp;q=80 640w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fbanner%20(1).jpg&amp;w=750&amp;q=80 750w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fbanner%20(1).jpg&amp;w=828&amp;q=80 828w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fbanner%20(1).jpg&amp;w=1080&amp;q=80 1080w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fbanner%20(1).jpg&amp;w=1200&amp;q=80 1200w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fbanner%20(1).jpg&amp;w=1920&amp;q=80 1920w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fbanner%20(1).jpg&amp;w=2048&amp;q=80 2048w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fbanner%20(1).jpg&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
<img alt="banner (1)" class="md:hidden" data-nimg="fill" decoding="async" fetchpriority="high" loading="eager" sizes="100vw" src="./Đào tạo tại UMC_files/banner%20(1).jpeg" srcset="/_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fbanner%20(1).jpg&amp;w=640&amp;q=80 640w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fbanner%20(1).jpg&amp;w=750&amp;q=80 750w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fbanner%20(1).jpg&amp;w=828&amp;q=80 828w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fbanner%20(1).jpg&amp;w=1080&amp;q=80 1080w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fbanner%20(1).jpg&amp;w=1200&amp;q=80 1200w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fbanner%20(1).jpg&amp;w=1920&amp;q=80 1920w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fbanner%20(1).jpg&amp;w=2048&amp;q=80 2048w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fbanner%20(1).jpg&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
<div class="absolute bottom-0 left-0 w-full h-[408px] bg-linear-service">
</div>
<div class="relative h-full pb-8 lg:pb-[78px] max-lg:flex max-lg:items-end">
<div class="container lg:h-full">
<div class="grid lg:grid-cols-2 h-full gap-y-6">
<div class="md:space-y-[20px] space-y-3 xl:space-y-[26px] max-w-[576px] w-full mt-auto">
<h1 class="display-1 text-white">
Đào tạo
</h1>
</div>
</div>
</div>
</div>
</section>
<section class="xl:py-8 md:py-6 py-4">
<div class="container xl:space-y-8 md:space-y-6 space-y-4">
<div class="prose prose-content">
<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, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";font-size:16px;font-style:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-weight:400;letter-spacing:normal;margin:0px 0px 12px;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;'>
Với chức năng là cơ sở giảng dạy thực hành, đào tạo là một trong những hoạt động nòng cốt của Bệnh viện Đại học Y Dược TP. Hồ Chí Minh, nhằm tối ưu công tác đào tạo, Bệnh viện đa dạng hóa các loại hình đào tạo để đáp ứng các yêu cầu về đào tạo của nội bộ, của các cơ sở y tế khác và của học viên nước ngoài.Với nguồn nhân lực dồi dào và trình độ chuyên môn cao, các chuyên khoa lâm sàng và cận lâm sàng lớn mạnh, số lượng người bệnh đến khám và chữa bệnh tăng cao, Bệnh viện Đại học Y Dược TP. Hồ Chí Minh còn là nơi thực hành không những của nhân viên y tế Việt Nam, mà còn là sự lựa chọn của rất nhiều sinh viên các nước như: Nhật Bản, Úc, Hoa Kỳ, Thái Lan, CHLB Đức, Pháp, Bỉ, Áo, Đan Mạch, Ấn Độ, …
</div>
<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, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";font-size:16px;font-style:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-weight:400;letter-spacing:normal;margin:0px 0px 12px;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;'>
Ngoài ra, hình thức đào tạo theo hợp đồng/đào tạo theo nhiệm vụ chỉ đạo tuyến/đào tạo chuyển giao kỹ thuật và các khóa đào tạo chuyên môn nghiệp vụ khác của ngành y tế mà không thuộc hệ thống bằng cấp quốc gia cũng là một nhiệm vụ của Bệnh viện Đại học Y Dược TP. Hồ Chí Minh.
</div>
<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, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";font-size:16px;font-style:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-weight:400;letter-spacing:normal;margin:0px 0px 12px;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;'>
Sự phát triển liên tục của Bệnh viện Đại học Y Dược TP. Hồ Chí Minh nói chung và công tác đào tạo nói riêng, đã góp phần đáp ứng đáng kể nhu cầu bồi dưỡng kiến thức, giúp các Bệnh viện ứng dụng nhiều phương pháp, kỹ thuật mới trong khám, chữa bệnh, tăng cường chất lượng điều trị và chăm sóc người bệnh.
</div>
<div style='--tw-font-weight:700;-webkit-text-stroke-width:0px;border-style:solid;border-width:0px;box-sizing:border-box;color:rgb(13, 113, 186);font-family:Roboto, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";font-size:16px;font-style:normal;font-variant-caps:normal;font-variant-ligatures:normal;letter-spacing:normal;margin:0px 0px 4px;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;'>
<strong>
ĐỊNH HƯỚNG VỀ HOẠT ĐỘNG ĐÀO TẠO TRONG NHỮNG NĂM TỚI
</strong>
</div>
<div style='--tw-font-weight:700;-webkit-text-stroke-width:0px;border-style:solid;border-width:0px;box-sizing:border-box;color:rgb(13, 113, 186);font-family:Roboto, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";font-size:16px;font-style:normal;font-variant-caps:normal;font-variant-ligatures:normal;letter-spacing:normal;margin:0px 0px 12px;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;'>
<strong>
Về đào tạo chuyên môn, nghiệp vụ cho viên chức của Bệnh viện:
</strong>
</div>
<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, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";font-size:16px;font-style:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-weight:400;letter-spacing:normal;margin:0px 0px 12px;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;'>
Bệnh viện luôn có chương trình hỗ trợ cho nhân viên trong việc học tập chuyên môn nghiệp vụ và các khóa đào tạo chính quy. Ban Giám đốc định hướng mỗi Đơn vị sẽ tự xây dựng kế hoạch và lập dự toán kinh phí đào tạo cho nhân viên. Lãnh đạo Đơn vị sẽ tự quyết định quy mô và chi phí đào tạo cho nhân viên dựa trên định hướng phát triển chuyên môn. Phần kinh phí này sẽ được trích từ quỹ đào tạo và nghiên cứu khoa học của Bệnh viện. Chủ trương này giúp các Đơn vị chủ động hơn trong việc đào tạo nhân lực, phát triển chuyên môn của Đơn vị mình.
</div>
<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, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";font-size:16px;font-style:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-weight:400;letter-spacing:normal;margin:0px 0px 12px;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;'>
Ngoài những chương trình đào tạo trong nước, Bệnh viện tiếp tục tăng cường gửi viên chức đi đào tạo ở nước ngoài. Bên cạnh việc học hỏi nâng cao trình độ chuyên môn, đây cũng là cơ hội để mở rộng kết nối, hợp tác quốc tế tiến đến hội nhập.
</div>
<div style='--tw-font-weight:700;-webkit-text-stroke-width:0px;border-style:solid;border-width:0px;box-sizing:border-box;color:rgb(13, 113, 186);font-family:Roboto, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";font-size:16px;font-style:normal;font-variant-caps:normal;font-variant-ligatures:normal;letter-spacing:normal;margin:0px 0px 12px;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;'>
<strong>
Về đào tạo chuyên môn, nghiệp vụ cho nhân sự thuộc các cơ sở y tế khác:
</strong>
</div>
<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, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";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>
<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 class="lucide lucide-chevron-up size-4 -rotate-90" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m18 15-6-6-6 6">
</path>
</svg>
</button>
<div class="swiper swiper-initialized swiper-horizontal w-full swiper-backface-hidden">
<div class="swiper-wrapper">
<div class="swiper-slide swiper-slide-active max-w-[356px] !mr-2 lg:!mr-3 xl:!mr-4" data-swiper-slide-index="0" style="width: 356px; margin-right: 16px;">
<div class="relative rounded-lg aspect-[3/2] overflow-hidden">
<img alt="image-6" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/image-6.jpeg" srcset="/_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-6.jpg&amp;w=640&amp;q=80 640w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-6.jpg&amp;w=750&amp;q=80 750w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-6.jpg&amp;w=828&amp;q=80 828w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-6.jpg&amp;w=1080&amp;q=80 1080w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-6.jpg&amp;w=1200&amp;q=80 1200w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-6.jpg&amp;w=1920&amp;q=80 1920w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-6.jpg&amp;w=2048&amp;q=80 2048w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-6.jpg&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
</div>
<div class="swiper-slide swiper-slide-next max-w-[356px] !mr-2 lg:!mr-3 xl:!mr-4" data-swiper-slide-index="1" style="width: 356px; margin-right: 16px;">
<div class="relative rounded-lg aspect-[3/2] overflow-hidden">
<img alt="image-5" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/image-5.jpeg" srcset="/_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-5.jpg&amp;w=640&amp;q=80 640w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-5.jpg&amp;w=750&amp;q=80 750w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-5.jpg&amp;w=828&amp;q=80 828w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-5.jpg&amp;w=1080&amp;q=80 1080w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-5.jpg&amp;w=1200&amp;q=80 1200w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-5.jpg&amp;w=1920&amp;q=80 1920w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-5.jpg&amp;w=2048&amp;q=80 2048w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-5.jpg&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
</div>
<div class="swiper-slide max-w-[356px] !mr-2 lg:!mr-3 xl:!mr-4" data-swiper-slide-index="2" style="width: 356px; margin-right: 16px;">
<div class="relative rounded-lg aspect-[3/2] overflow-hidden">
<img alt="image-4" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/image-4.jpeg" srcset="/_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-4.jpg&amp;w=640&amp;q=80 640w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-4.jpg&amp;w=750&amp;q=80 750w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-4.jpg&amp;w=828&amp;q=80 828w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-4.jpg&amp;w=1080&amp;q=80 1080w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-4.jpg&amp;w=1200&amp;q=80 1200w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-4.jpg&amp;w=1920&amp;q=80 1920w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-4.jpg&amp;w=2048&amp;q=80 2048w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-4.jpg&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
</div>
<div class="swiper-slide max-w-[356px] !mr-2 lg:!mr-3 xl:!mr-4" data-swiper-slide-index="3" style="width: 356px; margin-right: 16px;">
<div class="relative rounded-lg aspect-[3/2] overflow-hidden">
<img alt="image-3" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/image-3.jpeg" srcset="/_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-3.jpg&amp;w=640&amp;q=80 640w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-3.jpg&amp;w=750&amp;q=80 750w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-3.jpg&amp;w=828&amp;q=80 828w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-3.jpg&amp;w=1080&amp;q=80 1080w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-3.jpg&amp;w=1200&amp;q=80 1200w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-3.jpg&amp;w=1920&amp;q=80 1920w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-3.jpg&amp;w=2048&amp;q=80 2048w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-3.jpg&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
</div>
<div class="swiper-slide max-w-[356px] !mr-2 lg:!mr-3 xl:!mr-4" data-swiper-slide-index="4" style="width: 356px; margin-right: 16px;">
<div class="relative rounded-lg aspect-[3/2] overflow-hidden">
<img alt="image-2" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/image-2.jpeg" srcset="/_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-2.jpg&amp;w=640&amp;q=80 640w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-2.jpg&amp;w=750&amp;q=80 750w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-2.jpg&amp;w=828&amp;q=80 828w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-2.jpg&amp;w=1080&amp;q=80 1080w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-2.jpg&amp;w=1200&amp;q=80 1200w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-2.jpg&amp;w=1920&amp;q=80 1920w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-2.jpg&amp;w=2048&amp;q=80 2048w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-2.jpg&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
</div>
<div class="swiper-slide max-w-[356px] !mr-2 lg:!mr-3 xl:!mr-4" data-swiper-slide-index="5" style="width: 356px; margin-right: 16px;">
<div class="relative rounded-lg aspect-[3/2] overflow-hidden">
<img alt="image-1" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/image-1.jpeg" srcset="/_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-1.jpg&amp;w=640&amp;q=80 640w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-1.jpg&amp;w=750&amp;q=80 750w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-1.jpg&amp;w=828&amp;q=80 828w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-1.jpg&amp;w=1080&amp;q=80 1080w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-1.jpg&amp;w=1200&amp;q=80 1200w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-1.jpg&amp;w=1920&amp;q=80 1920w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-1.jpg&amp;w=2048&amp;q=80 2048w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FDemo%2FDemo%2Fimage-1.jpg&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
</div>
</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">
<svg class="lucide lucide-chevron-down size-4 -rotate-90" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m6 9 6 6 6-6">
</path>
</svg>
</button>
</div>
<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-[''] [&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">
<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">
<div>
<h2 class="display-7 text-primary-600">
Cấp chứng chỉ
</h2>
</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="">
<svg class="lucide lucide-chevron-up size-4 -rotate-90" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<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="">
<svg class="lucide lucide-chevron-down size-4 -rotate-90" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m6 9 6 6 6-6">
</path>
</svg>
</button>
</div>
</div>
</div>
<div class="relative max-md:px-4 md:mb-6 mb-4 lg:mb-8">
<div class="swiper swiper-initialized swiper-horizontal w-full swiper-backface-hidden">
<div class="swiper-wrapper">
<div class="swiper-slide swiper-slide-active h-auto max-w-[288px] mr-2 lg:!mr-3 xl:!mr-4" style="width: 288px; margin-right: 16px;">
<article class="relative p-3 lg:p-4 bg-white rounded-lg border border-gray-100 space-y-[8px] md:space-y-[14px] xl:space-y-[18px] group">
<a class="relative overflow-hidden rounded aspect-9/5 block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/chuong-trinh-chung-chi-dao-tao-ky-thuat-chuyen-mon-noi-soi-da-day-noi-soi-dai-trang-va-noi-soi-dieu-tri-co-ban-khoa-02">
<img alt="CHƯƠNG TRÌNH CHỨNG CHỈ ĐÀO TẠO KỸ THUẬT CHUYÊN MÔN: NỘI SOI DẠ DÀY, NỘI SOI ĐẠI TRÀNG VÀ NỘI SOI ĐIỀU TRỊ CƠ BẢN, KHÓA 02" class="object-cover h-full w-full" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/N%E1%BB%98I-SOI-D%E1%BA%A0-D%C3%80Y,-N%E1%BB%98I-SOI-%C4%90%E1%BA%A0I-TR%C3%80NG-V%C3%80-N%E1%BB%98I-SOI-%C4%90I%E1%BB%80U-TR%E1%BB%8A-C%C6%.gif" srcset="/_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FN%E1%BB%98I-SOI-D%E1%BA%A0-D%C3%80Y%2C-N%E1%BB%98I-SOI-%C4%90%E1%BA%A0I-TR%C3%80NG-V%C3%80-N%E1%BB%98I-SOI-%C4%90I%E1%BB%80U-TR%E1%BB%8A-C%C6%A0-B%E1%BA%A2N-(1).gif&amp;w=640&amp;q=80 640w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FN%E1%BB%98I-SOI-D%E1%BA%A0-D%C3%80Y%2C-N%E1%BB%98I-SOI-%C4%90%E1%BA%A0I-TR%C3%80NG-V%C3%80-N%E1%BB%98I-SOI-%C4%90I%E1%BB%80U-TR%E1%BB%8A-C%C6%A0-B%E1%BA%A2N-(1).gif&amp;w=750&amp;q=80 750w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FN%E1%BB%98I-SOI-D%E1%BA%A0-D%C3%80Y%2C-N%E1%BB%98I-SOI-%C4%90%E1%BA%A0I-TR%C3%80NG-V%C3%80-N%E1%BB%98I-SOI-%C4%90I%E1%BB%80U-TR%E1%BB%8A-C%C6%A0-B%E1%BA%A2N-(1).gif&amp;w=828&amp;q=80 828w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FN%E1%BB%98I-SOI-D%E1%BA%A0-D%C3%80Y%2C-N%E1%BB%98I-SOI-%C4%90%E1%BA%A0I-TR%C3%80NG-V%C3%80-N%E1%BB%98I-SOI-%C4%90I%E1%BB%80U-TR%E1%BB%8A-C%C6%A0-B%E1%BA%A2N-(1).gif&amp;w=1080&amp;q=80 1080w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FN%E1%BB%98I-SOI-D%E1%BA%A0-D%C3%80Y%2C-N%E1%BB%98I-SOI-%C4%90%E1%BA%A0I-TR%C3%80NG-V%C3%80-N%E1%BB%98I-SOI-%C4%90I%E1%BB%80U-TR%E1%BB%8A-C%C6%A0-B%E1%BA%A2N-(1).gif&amp;w=1200&amp;q=80 1200w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FN%E1%BB%98I-SOI-D%E1%BA%A0-D%C3%80Y%2C-N%E1%BB%98I-SOI-%C4%90%E1%BA%A0I-TR%C3%80NG-V%C3%80-N%E1%BB%98I-SOI-%C4%90I%E1%BB%80U-TR%E1%BB%8A-C%C6%A0-B%E1%BA%A2N-(1).gif&amp;w=1920&amp;q=80 1920w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FN%E1%BB%98I-SOI-D%E1%BA%A0-D%C3%80Y%2C-N%E1%BB%98I-SOI-%C4%90%E1%BA%A0I-TR%C3%80NG-V%C3%80-N%E1%BB%98I-SOI-%C4%90I%E1%BB%80U-TR%E1%BB%8A-C%C6%A0-B%E1%BA%A2N-(1).gif&amp;w=2048&amp;q=80 2048w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FN%E1%BB%98I-SOI-D%E1%BA%A0-D%C3%80Y%2C-N%E1%BB%98I-SOI-%C4%90%E1%BA%A0I-TR%C3%80NG-V%C3%80-N%E1%BB%98I-SOI-%C4%90I%E1%BB%80U-TR%E1%BB%8A-C%C6%A0-B%E1%BA%A2N-(1).gif&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
<div class="absolute inset-0 w-full h-full bg-black/40 opacity-0 lg:group-hover:opacity-100 duration-300 ease-in-out">
</div>
</a>
<div class="md:px-1.5 w-full space-y-[8px] md:space-y-[14px] xl:space-y-[18px]">
<div class="w-full space-y-1">
<a class="title-1 max-md:!text-[14px] line-clamp-2 md:h-[54px] h-[42px] lg:group-hover:text-primary-600 duration-300 ease-in-out block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/chuong-trinh-chung-chi-dao-tao-ky-thuat-chuyen-mon-noi-soi-da-day-noi-soi-dai-trang-va-noi-soi-dieu-tri-co-ban-khoa-02">
CHƯƠNG TRÌNH CHỨNG CHỈ ĐÀO TẠO KỸ THUẬT CHUYÊN MÔN: NỘI SOI DẠ DÀY, NỘI SOI ĐẠI TRÀNG VÀ NỘI SOI ĐIỀU TRỊ CƠ BẢN, KHÓA 02
</a>
<div class="body-3 text-gray-700 md:line-clamp-3 max-md:hidden h-[72px] md:h-[64px]">
Nội soi dạ dày là phương pháp chẩn đoán xâm lấn tối thiểu giúp ngăn ngừa thương tổn tiến triển thành ung thư dạ dày tiến triển. Ngoài khả năng giúp chẩn đoán các bệnh lý tiêu hóa và các ung thư sớm ống tiêu hóa, nội soi dạ dày còn giúp điều trị triệt căn các thương tổn này. Nội soi đại tràng hiện tại đang là tiêu chuẩn vàng trong tầm soát và điều trị xâm lấn tối thiểu các thương tổn tân sinh và các ung thư sớm của đại trực tràng. Nội soi đại tràng cũng là một kỹ thuật nội soi khó do đó đòi hỏi người bác sỹ phải được đào tạo tốt. Mục đích của khóa học nhằm cung cấp kiến thức về chẩn đoán và điều trị các bệnh tiêu hóa thường gặp, kỹ thuật nội soi dạ dày, nội soi đại tràng, các quan điểm mới trong nội soi chẩn đoán và điều trị các bệnh lý tiêu hóa, kỹ thuật nội soi điều trị ung thư sớm đường tiêu hoá như EMR, ESD, kỹ thuật cơ bản trong nội soi cầm máu các trường hợp chảy máu tiêu hóa. Trong khóa học, các giảng viên còn tập trung vào trình bày các quan điểm mới trong nội soi chẩn đoán và điều trị, các tai biến và biến chứng có thể xảy ra.
</div>
</div>
<div class="w-full h-px bg-gray-100">
</div>
<div class="flex justify-between items-center w-full">
<div class="flex md:gap-4 gap-2 items-center self-stretch my-auto">
<div class="flex md:gap-1.5 gap-1 items-center self-stretch my-auto">
<svg class="lucide lucide-calendar size-3.5" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2v4">
</path>
<path d="M16 2v4">
</path>
<rect height="18" rx="2" width="18" x="3" y="4">
</rect>
<path d="M3 10h18">
</path>
</svg>
<time class="self-stretch body-3 max-md:!text-[12px] my-auto">
27/07/2026
</time>
</div>
<div class="relative overflow-hidden w-9 aspect-9/5">
<img alt="icon" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/sat-image.jpeg" srcset="/_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=640&amp;q=80 640w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=750&amp;q=80 750w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=828&amp;q=80 828w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1080&amp;q=80 1080w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1200&amp;q=80 1200w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1920&amp;q=80 1920w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=2048&amp;q=80 2048w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
</div>
</div>
</div>
<a class="p-2 lg:p-3 w-full text-white bg-primary-600 rounded lg:hover:bg-primary-300 duration-300 ease-in-out block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/chuong-trinh-chung-chi-dao-tao-ky-thuat-chuyen-mon-noi-soi-da-day-noi-soi-dai-trang-va-noi-soi-dieu-tri-co-ban-khoa-02">
<div class="flex items-center gap-0.5 justify-center w-full heading-4 max-md:!text-[14px]">
<span>
38.000.000
</span>
<span>
VNĐ
</span>
</div>
</a>
</article>
</div>
<div class="swiper-slide swiper-slide-next h-auto max-w-[288px] mr-2 lg:!mr-3 xl:!mr-4" style="width: 288px; margin-right: 16px;">
<article class="relative p-3 lg:p-4 bg-white rounded-lg border border-gray-100 space-y-[8px] md:space-y-[14px] xl:space-y-[18px] group">
<a class="relative overflow-hidden rounded aspect-9/5 block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/chuong-trinh-chung-chi-dao-tao-ky-thuat-chuyen-mon-hoi-suc-ngoai-than-kinh-co-ban-khoa-05">
<img alt="CHƯƠNG TRÌNH CHỨNG CHỈ ĐÀO TẠO KỸ THUẬT CHUYÊN MÔN: HỒI SỨC NGOẠI THẦN KINH CƠ BẢN, KHÓA 05" class="object-cover h-full w-full" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/745940994_1567151031863709_1131070052084530832_n.jpeg" srcset="/_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2F745940994_1567151031863709_1131070052084530832_n.jpg&amp;w=640&amp;q=80 640w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2F745940994_1567151031863709_1131070052084530832_n.jpg&amp;w=750&amp;q=80 750w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2F745940994_1567151031863709_1131070052084530832_n.jpg&amp;w=828&amp;q=80 828w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2F745940994_1567151031863709_1131070052084530832_n.jpg&amp;w=1080&amp;q=80 1080w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2F745940994_1567151031863709_1131070052084530832_n.jpg&amp;w=1200&amp;q=80 1200w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2F745940994_1567151031863709_1131070052084530832_n.jpg&amp;w=1920&amp;q=80 1920w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2F745940994_1567151031863709_1131070052084530832_n.jpg&amp;w=2048&amp;q=80 2048w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2F745940994_1567151031863709_1131070052084530832_n.jpg&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
<div class="absolute inset-0 w-full h-full bg-black/40 opacity-0 lg:group-hover:opacity-100 duration-300 ease-in-out">
</div>
</a>
<div class="md:px-1.5 w-full space-y-[8px] md:space-y-[14px] xl:space-y-[18px]">
<div class="w-full space-y-1">
<a class="title-1 max-md:!text-[14px] line-clamp-2 md:h-[54px] h-[42px] lg:group-hover:text-primary-600 duration-300 ease-in-out block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/chuong-trinh-chung-chi-dao-tao-ky-thuat-chuyen-mon-hoi-suc-ngoai-than-kinh-co-ban-khoa-05">
CHƯƠNG TRÌNH CHỨNG CHỈ ĐÀO TẠO KỸ THUẬT CHUYÊN MÔN: HỒI SỨC NGOẠI THẦN KINH CƠ BẢN, KHÓA 05
</a>
<div class="body-3 text-gray-700 md:line-clamp-3 max-md:hidden h-[72px] md:h-[64px]">
Hồi sức Ngoại thần kinh là một lĩnh vực chuyên sâu, áp dụng các biện pháp chẩn đoán, điều trị nhằm duy trì, ổn định và cải thiện các chức năng sống của người bị các bệnh lý liên quan đến yếu tố thần kinh ở vùng não bộ, cột sống và dây thần kinh ngoại biên. Hồi sức giúp điều chỉnh các rối loạn về các chức năng sống của bệnh nhờ vào các phương tiện theo dõi, hỗ trợ và bằng việc sử dụng các thuốc hồi sức. Hồi sức Ngoại thần kinh đóng vai trò quan trọng toàn bộ các biện pháp chăm sóc tích cực nhằm giải quyết sự mất cân bằng của môi trường bên trong cơ thể và phục hồi các chức năng sinh tồn ở người bị bệnh lý Ngoại thần kinh.
</div>
</div>
<div class="w-full h-px bg-gray-100">
</div>
<div class="flex justify-between items-center w-full">
<div class="flex md:gap-4 gap-2 items-center self-stretch my-auto">
<div class="flex md:gap-1.5 gap-1 items-center self-stretch my-auto">
<svg class="lucide lucide-calendar size-3.5" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2v4">
</path>
<path d="M16 2v4">
</path>
<rect height="18" rx="2" width="18" x="3" y="4">
</rect>
<path d="M3 10h18">
</path>
</svg>
<time class="self-stretch body-3 max-md:!text-[12px] my-auto">
29/07/2026
</time>
</div>
<div class="relative overflow-hidden w-9 aspect-9/5">
<img alt="icon" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/sat-image.jpeg" srcset="/_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=640&amp;q=80 640w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=750&amp;q=80 750w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=828&amp;q=80 828w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1080&amp;q=80 1080w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1200&amp;q=80 1200w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1920&amp;q=80 1920w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=2048&amp;q=80 2048w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
</div>
</div>
</div>
<a class="p-2 lg:p-3 w-full text-white bg-primary-600 rounded lg:hover:bg-primary-300 duration-300 ease-in-out block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/chuong-trinh-chung-chi-dao-tao-ky-thuat-chuyen-mon-hoi-suc-ngoai-than-kinh-co-ban-khoa-05">
<div class="flex items-center gap-0.5 justify-center w-full heading-4 max-md:!text-[14px]">
<span>
60.000.000
</span>
<span>
VNĐ
</span>
</div>
</a>
</article>
</div>
</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">
<div>
<h2 class="display-7 text-primary-600">
Cấp giấy chứng nhận
</h2>
</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="">
<svg class="lucide lucide-chevron-up size-4 -rotate-90" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<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="">
<svg class="lucide lucide-chevron-down size-4 -rotate-90" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m6 9 6 6 6-6">
</path>
</svg>
</button>
</div>
</div>
</div>
<div class="relative max-md:px-4 md:mb-6 mb-4 lg:mb-8">
<div class="swiper swiper-initialized swiper-horizontal w-full swiper-backface-hidden">
<div class="swiper-wrapper">
<div class="swiper-slide swiper-slide-active h-auto max-w-[288px] mr-2 lg:!mr-3 xl:!mr-4" style="width: 288px; margin-right: 16px;">
<article class="relative p-3 lg:p-4 bg-white rounded-lg border border-gray-100 space-y-[8px] md:space-y-[14px] xl:space-y-[18px] group">
<a class="relative overflow-hidden rounded aspect-9/5 block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/lop-phau-thuat-noi-soi-cat-da-day">
<img alt="Lớp: PHẪU THUẬT NỘI SOI CẮT DẠ DÀY" class="object-cover h-full w-full" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/PTNS2707.jpeg" srcset="/_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FPTNS2707.jpg&amp;w=640&amp;q=80 640w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FPTNS2707.jpg&amp;w=750&amp;q=80 750w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FPTNS2707.jpg&amp;w=828&amp;q=80 828w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FPTNS2707.jpg&amp;w=1080&amp;q=80 1080w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FPTNS2707.jpg&amp;w=1200&amp;q=80 1200w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FPTNS2707.jpg&amp;w=1920&amp;q=80 1920w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FPTNS2707.jpg&amp;w=2048&amp;q=80 2048w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FPTNS2707.jpg&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
<div class="absolute inset-0 w-full h-full bg-black/40 opacity-0 lg:group-hover:opacity-100 duration-300 ease-in-out">
</div>
</a>
<div class="md:px-1.5 w-full space-y-[8px] md:space-y-[14px] xl:space-y-[18px]">
<div class="w-full space-y-1">
<a class="title-1 max-md:!text-[14px] line-clamp-2 md:h-[54px] h-[42px] lg:group-hover:text-primary-600 duration-300 ease-in-out block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/lop-phau-thuat-noi-soi-cat-da-day">
Lớp: PHẪU THUẬT NỘI SOI CẮT DẠ DÀY
</a>
<div class="body-3 text-gray-700 md:line-clamp-3 max-md:hidden h-[72px] md:h-[64px]">
Căn cứ nhu cầu đào tạo về phẫu thuật nội soi cắt dạ dày cho các bác sĩ ở Việt Nam. Nhằm cập nhật kiến thức, kỹ năng về lĩnh vực ung thư dạ dày
</div>
</div>
<div class="w-full h-px bg-gray-100">
</div>
<div class="flex justify-between items-center w-full">
<div class="flex md:gap-4 gap-2 items-center self-stretch my-auto">
<div class="flex md:gap-1.5 gap-1 items-center self-stretch my-auto">
<svg class="lucide lucide-calendar size-3.5" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2v4">
</path>
<path d="M16 2v4">
</path>
<rect height="18" rx="2" width="18" x="3" y="4">
</rect>
<path d="M3 10h18">
</path>
</svg>
<time class="self-stretch body-3 max-md:!text-[12px] my-auto">
27/07/2026
</time>
</div>
<div class="relative overflow-hidden w-9 aspect-9/5">
<img alt="icon" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/sat-image.jpeg" srcset="/_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=640&amp;q=80 640w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=750&amp;q=80 750w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=828&amp;q=80 828w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1080&amp;q=80 1080w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1200&amp;q=80 1200w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1920&amp;q=80 1920w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=2048&amp;q=80 2048w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
</div>
</div>
</div>
<a class="p-2 lg:p-3 w-full text-white bg-primary-600 rounded lg:hover:bg-primary-300 duration-300 ease-in-out block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/lop-phau-thuat-noi-soi-cat-da-day">
<div class="flex items-center gap-0.5 justify-center w-full heading-4 max-md:!text-[14px]">
<span>
8.000.000
</span>
<span>
VNĐ
</span>
</div>
</a>
</article>
</div>
<div class="swiper-slide swiper-slide-next h-auto max-w-[288px] mr-2 lg:!mr-3 xl:!mr-4" style="width: 288px; margin-right: 16px;">
<article class="relative p-3 lg:p-4 bg-white rounded-lg border border-gray-100 space-y-[8px] md:space-y-[14px] xl:space-y-[18px] group">
<a class="relative overflow-hidden rounded aspect-9/5 block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/lop-dieu-duong-dung-cu-trong-phau-thuat-mo-mo-nang-cao">
<img alt="Lớp: ĐIỀU DƯỠNG DỤNG CỤ TRONG PHẪU THUẬT (MỔ MỞ) NÂNG CAO" class="object-cover h-full w-full" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/DD%2003-14.jpeg" srcset="/_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FDD%2003-14.08.jpg&amp;w=640&amp;q=80 640w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FDD%2003-14.08.jpg&amp;w=750&amp;q=80 750w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FDD%2003-14.08.jpg&amp;w=828&amp;q=80 828w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FDD%2003-14.08.jpg&amp;w=1080&amp;q=80 1080w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FDD%2003-14.08.jpg&amp;w=1200&amp;q=80 1200w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FDD%2003-14.08.jpg&amp;w=1920&amp;q=80 1920w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FDD%2003-14.08.jpg&amp;w=2048&amp;q=80 2048w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2FDD%2003-14.08.jpg&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
<div class="absolute inset-0 w-full h-full bg-black/40 opacity-0 lg:group-hover:opacity-100 duration-300 ease-in-out">
</div>
</a>
<div class="md:px-1.5 w-full space-y-[8px] md:space-y-[14px] xl:space-y-[18px]">
<div class="w-full space-y-1">
<a class="title-1 max-md:!text-[14px] line-clamp-2 md:h-[54px] h-[42px] lg:group-hover:text-primary-600 duration-300 ease-in-out block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/lop-dieu-duong-dung-cu-trong-phau-thuat-mo-mo-nang-cao">
Lớp: ĐIỀU DƯỠNG DỤNG CỤ TRONG PHẪU THUẬT (MỔ MỞ) NÂNG CAO
</a>
<div class="body-3 text-gray-700 md:line-clamp-3 max-md:hidden h-[72px] md:h-[64px]">
Nhằm cung cấp kiến thức chuyên môn và trao đổi kinh nghiệm thực hành trong lĩnh vực chăm sóc người bệnh trong phẫu thuật của điều dưỡng dụng cụ nhằm hỗ trợ bác sĩ trong tiến trình phẫu thuật,
</div>
</div>
<div class="w-full h-px bg-gray-100">
</div>
<div class="flex justify-between items-center w-full">
<div class="flex md:gap-4 gap-2 items-center self-stretch my-auto">
<div class="flex md:gap-1.5 gap-1 items-center self-stretch my-auto">
<svg class="lucide lucide-calendar size-3.5" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2v4">
</path>
<path d="M16 2v4">
</path>
<rect height="18" rx="2" width="18" x="3" y="4">
</rect>
<path d="M3 10h18">
</path>
</svg>
<time class="self-stretch body-3 max-md:!text-[12px] my-auto">
03/08/2026
</time>
</div>
<div class="relative overflow-hidden w-9 aspect-9/5">
<img alt="icon" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/sat-image.jpeg" srcset="/_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=640&amp;q=80 640w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=750&amp;q=80 750w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=828&amp;q=80 828w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1080&amp;q=80 1080w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1200&amp;q=80 1200w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1920&amp;q=80 1920w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=2048&amp;q=80 2048w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
</div>
</div>
</div>
<a class="p-2 lg:p-3 w-full text-white bg-primary-600 rounded lg:hover:bg-primary-300 duration-300 ease-in-out block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/lop-dieu-duong-dung-cu-trong-phau-thuat-mo-mo-nang-cao">
<div class="flex items-center gap-0.5 justify-center w-full heading-4 max-md:!text-[14px]">
<span>
6.000.000
</span>
<span>
VNĐ
</span>
</div>
</a>
</article>
</div>
<div class="swiper-slide h-auto max-w-[288px] mr-2 lg:!mr-3 xl:!mr-4" style="width: 288px; margin-right: 16px;">
<article class="relative p-3 lg:p-4 bg-white rounded-lg border border-gray-100 space-y-[8px] md:space-y-[14px] xl:space-y-[18px] group">
<a class="relative overflow-hidden rounded aspect-9/5 block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/lop-dieu-duong-ky-thuat-y-trong-thuc-hanh-lam-sang-benh-ly-sa-sut-tri-tue-khoa-01-1">
<img alt="Lớp: ĐIỀU DƯỠNG, KỸ THUẬT Y TRONG THỰC HÀNH LÂM SÀNG BỆNH LÝ SA SÚT TRÍ TUỆ, KHÓA 01" class="object-cover h-full w-full" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/chieusinhdieuduong-0308.gif" srcset="/_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2Fchieusinhdieuduong-0308.gif&amp;w=640&amp;q=80 640w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2Fchieusinhdieuduong-0308.gif&amp;w=750&amp;q=80 750w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2Fchieusinhdieuduong-0308.gif&amp;w=828&amp;q=80 828w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2Fchieusinhdieuduong-0308.gif&amp;w=1080&amp;q=80 1080w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2Fchieusinhdieuduong-0308.gif&amp;w=1200&amp;q=80 1200w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2Fchieusinhdieuduong-0308.gif&amp;w=1920&amp;q=80 1920w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2Fchieusinhdieuduong-0308.gif&amp;w=2048&amp;q=80 2048w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2FT%E1%BB%95%20S%E1%BB%B1%20Ki%E1%BB%87n%202026%2F07%2Fchieusinhdieuduong-0308.gif&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
<div class="absolute inset-0 w-full h-full bg-black/40 opacity-0 lg:group-hover:opacity-100 duration-300 ease-in-out">
</div>
</a>
<div class="md:px-1.5 w-full space-y-[8px] md:space-y-[14px] xl:space-y-[18px]">
<div class="w-full space-y-1">
<a class="title-1 max-md:!text-[14px] line-clamp-2 md:h-[54px] h-[42px] lg:group-hover:text-primary-600 duration-300 ease-in-out block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/lop-dieu-duong-ky-thuat-y-trong-thuc-hanh-lam-sang-benh-ly-sa-sut-tri-tue-khoa-01-1">
Lớp: ĐIỀU DƯỠNG, KỸ THUẬT Y TRONG THỰC HÀNH LÂM SÀNG BỆNH LÝ SA SÚT TRÍ TUỆ, KHÓA 01
</a>
<div class="body-3 text-gray-700 md:line-clamp-3 max-md:hidden h-[72px] md:h-[64px]">
Sa sút trí tuệ là một lĩnh vực chuyên sâu và đặc thù, thuộc chuyên ngành Thần kinh, sử dụng nhiều phương pháp đánh giá và điều trị như các test đánh giá nhận thức, chăm sóc và theo dõi người bệnh, tập luyện và kích thích nhận thức, kích thích từ trường xuyên sọ, hoạt động trị liệu, tư vấn và hỗ trợ người chăm sóc. Để triển khai hiệu quả mô hình tiếp cận chẩn đoán, quản lý và điều trị toàn diện người bệnh sa sút trí tuệ cần có sự phối hợp của nhiều nhóm nhân sự khác nhau như bác sĩ, điều dưỡng, kỹ thuật viên tâm lý, vật lý trị liệu, hoạt động trị liệu và các nhân sự hỗ trợ khác.
</div>
</div>
<div class="w-full h-px bg-gray-100">
</div>
<div class="flex justify-between items-center w-full">
<div class="flex md:gap-4 gap-2 items-center self-stretch my-auto">
<div class="flex md:gap-1.5 gap-1 items-center self-stretch my-auto">
<svg class="lucide lucide-calendar size-3.5" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2v4">
</path>
<path d="M16 2v4">
</path>
<rect height="18" rx="2" width="18" x="3" y="4">
</rect>
<path d="M3 10h18">
</path>
</svg>
<time class="self-stretch body-3 max-md:!text-[12px] my-auto">
03/08/2026
</time>
</div>
<div class="relative overflow-hidden w-9 aspect-9/5">
<img alt="icon" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/sat-image.jpeg" srcset="/_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=640&amp;q=80 640w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=750&amp;q=80 750w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=828&amp;q=80 828w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1080&amp;q=80 1080w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1200&amp;q=80 1200w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1920&amp;q=80 1920w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=2048&amp;q=80 2048w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
</div>
</div>
</div>
<a class="p-2 lg:p-3 w-full text-white bg-primary-600 rounded lg:hover:bg-primary-300 duration-300 ease-in-out block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/lop-dieu-duong-ky-thuat-y-trong-thuc-hanh-lam-sang-benh-ly-sa-sut-tri-tue-khoa-01-1">
<div class="flex items-center gap-0.5 justify-center w-full heading-4 max-md:!text-[14px]">
<span>
15.000.000
</span>
<span>
VNĐ
</span>
</div>
</a>
</article>
</div>
</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">
<div>
<h2 class="display-7 text-primary-600">
Đào tạo cấp giấy xác nhận quá trình thực hành
</h2>
</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="">
<svg class="lucide lucide-chevron-up size-4 -rotate-90" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<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="">
<svg class="lucide lucide-chevron-down size-4 -rotate-90" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m6 9 6 6 6-6">
</path>
</svg>
</button>
</div>
</div>
</div>
<div class="relative max-md:px-4 md:mb-6 mb-4 lg:mb-8">
<div class="swiper swiper-initialized swiper-horizontal w-full swiper-backface-hidden">
<div class="swiper-wrapper">
<div class="swiper-slide swiper-slide-active h-auto max-w-[288px] mr-2 lg:!mr-3 xl:!mr-4" style="width: 288px; margin-right: 16px;">
<article class="relative p-3 lg:p-4 bg-white rounded-lg border border-gray-100 space-y-[8px] md:space-y-[14px] xl:space-y-[18px] group">
<a class="relative overflow-hidden rounded aspect-9/5 block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/chuong-trinh-thuc-hanh-12-thang-bac-si-y-khoa">
<img alt="Chương trình thực hành 12 tháng Bác sĩ y khoa" class="object-cover h-full w-full" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/CT_Thuc%20hanh%2012%20thang_13122025.jpeg" srcset="/_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FChuye%CC%82n%20gia%20Y%20te%CC%82%CC%81%2F%C4%90a%CC%80o%20ta%CC%A3o%2FCa%CC%82%CC%81p%20chu%CC%9B%CC%81ng%20nha%CC%A3%CC%82n%2FCT_Thuc%20hanh%2012%20thang_13122025.jpg&amp;w=640&amp;q=80 640w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FChuye%CC%82n%20gia%20Y%20te%CC%82%CC%81%2F%C4%90a%CC%80o%20ta%CC%A3o%2FCa%CC%82%CC%81p%20chu%CC%9B%CC%81ng%20nha%CC%A3%CC%82n%2FCT_Thuc%20hanh%2012%20thang_13122025.jpg&amp;w=750&amp;q=80 750w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FChuye%CC%82n%20gia%20Y%20te%CC%82%CC%81%2F%C4%90a%CC%80o%20ta%CC%A3o%2FCa%CC%82%CC%81p%20chu%CC%9B%CC%81ng%20nha%CC%A3%CC%82n%2FCT_Thuc%20hanh%2012%20thang_13122025.jpg&amp;w=828&amp;q=80 828w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FChuye%CC%82n%20gia%20Y%20te%CC%82%CC%81%2F%C4%90a%CC%80o%20ta%CC%A3o%2FCa%CC%82%CC%81p%20chu%CC%9B%CC%81ng%20nha%CC%A3%CC%82n%2FCT_Thuc%20hanh%2012%20thang_13122025.jpg&amp;w=1080&amp;q=80 1080w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FChuye%CC%82n%20gia%20Y%20te%CC%82%CC%81%2F%C4%90a%CC%80o%20ta%CC%A3o%2FCa%CC%82%CC%81p%20chu%CC%9B%CC%81ng%20nha%CC%A3%CC%82n%2FCT_Thuc%20hanh%2012%20thang_13122025.jpg&amp;w=1200&amp;q=80 1200w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FChuye%CC%82n%20gia%20Y%20te%CC%82%CC%81%2F%C4%90a%CC%80o%20ta%CC%A3o%2FCa%CC%82%CC%81p%20chu%CC%9B%CC%81ng%20nha%CC%A3%CC%82n%2FCT_Thuc%20hanh%2012%20thang_13122025.jpg&amp;w=1920&amp;q=80 1920w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FChuye%CC%82n%20gia%20Y%20te%CC%82%CC%81%2F%C4%90a%CC%80o%20ta%CC%A3o%2FCa%CC%82%CC%81p%20chu%CC%9B%CC%81ng%20nha%CC%A3%CC%82n%2FCT_Thuc%20hanh%2012%20thang_13122025.jpg&amp;w=2048&amp;q=80 2048w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FChuye%CC%82n%20gia%20Y%20te%CC%82%CC%81%2F%C4%90a%CC%80o%20ta%CC%A3o%2FCa%CC%82%CC%81p%20chu%CC%9B%CC%81ng%20nha%CC%A3%CC%82n%2FCT_Thuc%20hanh%2012%20thang_13122025.jpg&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
<div class="absolute inset-0 w-full h-full bg-black/40 opacity-0 lg:group-hover:opacity-100 duration-300 ease-in-out">
</div>
</a>
<div class="md:px-1.5 w-full space-y-[8px] md:space-y-[14px] xl:space-y-[18px]">
<div class="w-full space-y-1">
<a class="title-1 max-md:!text-[14px] line-clamp-2 md:h-[54px] h-[42px] lg:group-hover:text-primary-600 duration-300 ease-in-out block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/chuong-trinh-thuc-hanh-12-thang-bac-si-y-khoa">
Chương trình thực hành 12 tháng Bác sĩ y khoa
</a>
<div class="body-3 text-gray-700 md:line-clamp-3 max-md:hidden h-[72px] md:h-[64px]">
Bệnh viện Đại học Y Dược Thành phố Hồ Chí Minh thông báo chiêu sinh “Chương trình thực hành 12 tháng để cấp giấy phép hành nghề khám bệnh, chữa bệnh đối với chức danh bác sỹ y khoa”
</div>
</div>
<div class="w-full h-px bg-gray-100">
</div>
<div class="flex justify-between items-center w-full">
<div class="flex md:gap-4 gap-2 items-center self-stretch my-auto">
<div class="flex md:gap-1.5 gap-1 items-center self-stretch my-auto">
<svg class="lucide lucide-calendar size-3.5" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2v4">
</path>
<path d="M16 2v4">
</path>
<rect height="18" rx="2" width="18" x="3" y="4">
</rect>
<path d="M3 10h18">
</path>
</svg>
<time class="self-stretch body-3 max-md:!text-[12px] my-auto">
12/09/2026
</time>
</div>
<div class="relative overflow-hidden w-9 aspect-9/5">
<img alt="icon" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/sat-image.jpeg" srcset="/_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=640&amp;q=80 640w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=750&amp;q=80 750w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=828&amp;q=80 828w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1080&amp;q=80 1080w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1200&amp;q=80 1200w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1920&amp;q=80 1920w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=2048&amp;q=80 2048w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
</div>
</div>
</div>
<a class="p-2 lg:p-3 w-full text-white bg-primary-600 rounded lg:hover:bg-primary-300 duration-300 ease-in-out block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/chuong-trinh-thuc-hanh-12-thang-bac-si-y-khoa">
<div class="flex items-center gap-0.5 justify-center w-full heading-4 max-md:!text-[14px]">
<span>
42.400.000
</span>
<span>
VNĐ
</span>
</div>
</a>
</article>
</div>
<div class="swiper-slide swiper-slide-next h-auto max-w-[288px] mr-2 lg:!mr-3 xl:!mr-4" style="width: 288px; margin-right: 16px;">
<article class="relative p-3 lg:p-4 bg-white rounded-lg border border-gray-100 space-y-[8px] md:space-y-[14px] xl:space-y-[18px] group">
<a class="relative overflow-hidden rounded aspect-9/5 block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/chuong-trinh-thuc-hanh-lam-sang-cho-ho-sinh">
<img alt="Chương trình thực hành lâm sàng cho Hộ sinh" class="object-cover h-full w-full" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/Avatar%20Ho%20sinh%206M.png" srcset="/_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2F%C4%90%C3%A0o%20t%E1%BA%A1o%2FAvatar%20Ho%20sinh%206M.png&amp;w=640&amp;q=80 640w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2F%C4%90%C3%A0o%20t%E1%BA%A1o%2FAvatar%20Ho%20sinh%206M.png&amp;w=750&amp;q=80 750w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2F%C4%90%C3%A0o%20t%E1%BA%A1o%2FAvatar%20Ho%20sinh%206M.png&amp;w=828&amp;q=80 828w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2F%C4%90%C3%A0o%20t%E1%BA%A1o%2FAvatar%20Ho%20sinh%206M.png&amp;w=1080&amp;q=80 1080w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2F%C4%90%C3%A0o%20t%E1%BA%A1o%2FAvatar%20Ho%20sinh%206M.png&amp;w=1200&amp;q=80 1200w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2F%C4%90%C3%A0o%20t%E1%BA%A1o%2FAvatar%20Ho%20sinh%206M.png&amp;w=1920&amp;q=80 1920w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2F%C4%90%C3%A0o%20t%E1%BA%A1o%2FAvatar%20Ho%20sinh%206M.png&amp;w=2048&amp;q=80 2048w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2F%C4%90%C3%A0o%20t%E1%BA%A1o%2FAvatar%20Ho%20sinh%206M.png&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
<div class="absolute inset-0 w-full h-full bg-black/40 opacity-0 lg:group-hover:opacity-100 duration-300 ease-in-out">
</div>
</a>
<div class="md:px-1.5 w-full space-y-[8px] md:space-y-[14px] xl:space-y-[18px]">
<div class="w-full space-y-1">
<a class="title-1 max-md:!text-[14px] line-clamp-2 md:h-[54px] h-[42px] lg:group-hover:text-primary-600 duration-300 ease-in-out block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/chuong-trinh-thuc-hanh-lam-sang-cho-ho-sinh">
Chương trình thực hành lâm sàng cho Hộ sinh
</a>
<div class="body-3 text-gray-700 md:line-clamp-3 max-md:hidden h-[72px] md:h-[64px]">
Bệnh viện Đại học Y Dược Thành phố Hồ Chí Minh thông báo chiêu sinh “Chương trình thực hành lâm sàng cho Hộ sinh”
</div>
</div>
<div class="w-full h-px bg-gray-100">
</div>
<div class="flex justify-between items-center w-full">
<div class="flex md:gap-4 gap-2 items-center self-stretch my-auto">
<div class="flex md:gap-1.5 gap-1 items-center self-stretch my-auto">
<svg class="lucide lucide-calendar size-3.5" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2v4">
</path>
<path d="M16 2v4">
</path>
<rect height="18" rx="2" width="18" x="3" y="4">
</rect>
<path d="M3 10h18">
</path>
</svg>
<time class="self-stretch body-3 max-md:!text-[12px] my-auto">
15/10/2026
</time>
</div>
<div class="relative overflow-hidden w-9 aspect-9/5">
<img alt="icon" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/sat-image.jpeg" srcset="/_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=640&amp;q=80 640w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=750&amp;q=80 750w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=828&amp;q=80 828w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1080&amp;q=80 1080w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1200&amp;q=80 1200w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1920&amp;q=80 1920w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=2048&amp;q=80 2048w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
</div>
</div>
</div>
<a class="p-2 lg:p-3 w-full text-white bg-primary-600 rounded lg:hover:bg-primary-300 duration-300 ease-in-out block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/chuong-trinh-thuc-hanh-lam-sang-cho-ho-sinh">
<div class="flex items-center gap-0.5 justify-center w-full heading-4 max-md:!text-[14px]">
<span>
18.150.000
</span>
<span>
VNĐ
</span>
</div>
</a>
</article>
</div>
<div class="swiper-slide h-auto max-w-[288px] mr-2 lg:!mr-3 xl:!mr-4" style="width: 288px; margin-right: 16px;">
<article class="relative p-3 lg:p-4 bg-white rounded-lg border border-gray-100 space-y-[8px] md:space-y-[14px] xl:space-y-[18px] group">
<a class="relative overflow-hidden rounded aspect-9/5 block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/chuong-trinh-thuc-hanh-06-thang-de-cap-giay-phep-hanh-nghe-kham-benh-chua-benh-doi-voi-chuc-danh-ky-thuat-y-nam-2026">
<img alt="Chương trình thực hành 06 tháng để cấp giấy phép hành nghề khám bệnh, chữa bệnh đối với chức danh Kỹ thuật y năm 2026" class="object-cover h-full w-full" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/Ky-thuat-y-6-thang_15042025.jpeg" srcset="/_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FChuye%CC%82n%20gia%20Y%20te%CC%82%CC%81%2F%C4%90a%CC%80o%20ta%CC%A3o%2FCa%CC%82%CC%81p%20chu%CC%9B%CC%81ng%20nha%CC%A3%CC%82n%2FKy-thuat-y-6-thang_15042025.jpg&amp;w=640&amp;q=80 640w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FChuye%CC%82n%20gia%20Y%20te%CC%82%CC%81%2F%C4%90a%CC%80o%20ta%CC%A3o%2FCa%CC%82%CC%81p%20chu%CC%9B%CC%81ng%20nha%CC%A3%CC%82n%2FKy-thuat-y-6-thang_15042025.jpg&amp;w=750&amp;q=80 750w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FChuye%CC%82n%20gia%20Y%20te%CC%82%CC%81%2F%C4%90a%CC%80o%20ta%CC%A3o%2FCa%CC%82%CC%81p%20chu%CC%9B%CC%81ng%20nha%CC%A3%CC%82n%2FKy-thuat-y-6-thang_15042025.jpg&amp;w=828&amp;q=80 828w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FChuye%CC%82n%20gia%20Y%20te%CC%82%CC%81%2F%C4%90a%CC%80o%20ta%CC%A3o%2FCa%CC%82%CC%81p%20chu%CC%9B%CC%81ng%20nha%CC%A3%CC%82n%2FKy-thuat-y-6-thang_15042025.jpg&amp;w=1080&amp;q=80 1080w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FChuye%CC%82n%20gia%20Y%20te%CC%82%CC%81%2F%C4%90a%CC%80o%20ta%CC%A3o%2FCa%CC%82%CC%81p%20chu%CC%9B%CC%81ng%20nha%CC%A3%CC%82n%2FKy-thuat-y-6-thang_15042025.jpg&amp;w=1200&amp;q=80 1200w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FChuye%CC%82n%20gia%20Y%20te%CC%82%CC%81%2F%C4%90a%CC%80o%20ta%CC%A3o%2FCa%CC%82%CC%81p%20chu%CC%9B%CC%81ng%20nha%CC%A3%CC%82n%2FKy-thuat-y-6-thang_15042025.jpg&amp;w=1920&amp;q=80 1920w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FChuye%CC%82n%20gia%20Y%20te%CC%82%CC%81%2F%C4%90a%CC%80o%20ta%CC%A3o%2FCa%CC%82%CC%81p%20chu%CC%9B%CC%81ng%20nha%CC%A3%CC%82n%2FKy-thuat-y-6-thang_15042025.jpg&amp;w=2048&amp;q=80 2048w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FChuye%CC%82n%20gia%20Y%20te%CC%82%CC%81%2F%C4%90a%CC%80o%20ta%CC%A3o%2FCa%CC%82%CC%81p%20chu%CC%9B%CC%81ng%20nha%CC%A3%CC%82n%2FKy-thuat-y-6-thang_15042025.jpg&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
<div class="absolute inset-0 w-full h-full bg-black/40 opacity-0 lg:group-hover:opacity-100 duration-300 ease-in-out">
</div>
</a>
<div class="md:px-1.5 w-full space-y-[8px] md:space-y-[14px] xl:space-y-[18px]">
<div class="w-full space-y-1">
<a class="title-1 max-md:!text-[14px] line-clamp-2 md:h-[54px] h-[42px] lg:group-hover:text-primary-600 duration-300 ease-in-out block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/chuong-trinh-thuc-hanh-06-thang-de-cap-giay-phep-hanh-nghe-kham-benh-chua-benh-doi-voi-chuc-danh-ky-thuat-y-nam-2026">
Chương trình thực hành 06 tháng để cấp giấy phép hành nghề khám bệnh, chữa bệnh đối với chức danh Kỹ thuật y năm 2026
</a>
<div class="body-3 text-gray-700 md:line-clamp-3 max-md:hidden h-[72px] md:h-[64px]">
Bệnh viện Đại học Y Dược Thành phố Hồ Chí Minh thông báo chiêu sinh Chương trình thực hành 06 tháng để cấp giấy phép hành nghề khám bệnh, chữa bệnh đối với chức danh Kỹ thuật y
</div>
</div>
<div class="w-full h-px bg-gray-100">
</div>
<div class="flex justify-between items-center w-full">
<div class="flex md:gap-4 gap-2 items-center self-stretch my-auto">
<div class="flex md:gap-1.5 gap-1 items-center self-stretch my-auto">
<svg class="lucide lucide-calendar size-3.5" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2v4">
</path>
<path d="M16 2v4">
</path>
<rect height="18" rx="2" width="18" x="3" y="4">
</rect>
<path d="M3 10h18">
</path>
</svg>
<time class="self-stretch body-3 max-md:!text-[12px] my-auto">
15/10/2026
</time>
</div>
<div class="relative overflow-hidden w-9 aspect-9/5">
<img alt="icon" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/sat-image.jpeg" srcset="/_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=640&amp;q=80 640w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=750&amp;q=80 750w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=828&amp;q=80 828w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1080&amp;q=80 1080w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1200&amp;q=80 1200w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1920&amp;q=80 1920w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=2048&amp;q=80 2048w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
</div>
</div>
</div>
<a class="p-2 lg:p-3 w-full text-white bg-primary-600 rounded lg:hover:bg-primary-300 duration-300 ease-in-out block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/chuong-trinh-thuc-hanh-06-thang-de-cap-giay-phep-hanh-nghe-kham-benh-chua-benh-doi-voi-chuc-danh-ky-thuat-y-nam-2026">
<div class="flex items-center gap-0.5 justify-center w-full heading-4 max-md:!text-[14px]">
<span>
16.400.000
</span>
<span>
VNĐ
</span>
</div>
</a>
</article>
</div>
<div class="swiper-slide h-auto max-w-[288px] mr-2 lg:!mr-3 xl:!mr-4" style="width: 288px; margin-right: 16px;">
<article class="relative p-3 lg:p-4 bg-white rounded-lg border border-gray-100 space-y-[8px] md:space-y-[14px] xl:space-y-[18px] group">
<a class="relative overflow-hidden rounded aspect-9/5 block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/chuong-trinh-dao-tao-thuc-hanh-lam-sang-cho-dieu-duong">
<img alt="Chương trình đào tạo thực hành lâm sàng cho điều dưỡng" class="object-cover h-full w-full" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/Avatar%20Dieu%20duong%206M.jpeg" srcset="/_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2F%C4%90%C3%A0o%20t%E1%BA%A1o%2FAvatar%20Dieu%20duong%206M.JPG&amp;w=640&amp;q=80 640w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2F%C4%90%C3%A0o%20t%E1%BA%A1o%2FAvatar%20Dieu%20duong%206M.JPG&amp;w=750&amp;q=80 750w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2F%C4%90%C3%A0o%20t%E1%BA%A1o%2FAvatar%20Dieu%20duong%206M.JPG&amp;w=828&amp;q=80 828w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2F%C4%90%C3%A0o%20t%E1%BA%A1o%2FAvatar%20Dieu%20duong%206M.JPG&amp;w=1080&amp;q=80 1080w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2F%C4%90%C3%A0o%20t%E1%BA%A1o%2FAvatar%20Dieu%20duong%206M.JPG&amp;w=1200&amp;q=80 1200w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2F%C4%90%C3%A0o%20t%E1%BA%A1o%2FAvatar%20Dieu%20duong%206M.JPG&amp;w=1920&amp;q=80 1920w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2F%C4%90%C3%A0o%20t%E1%BA%A1o%2FAvatar%20Dieu%20duong%206M.JPG&amp;w=2048&amp;q=80 2048w, /_next/image?url=https%3A%2F%2Fconsole.bvdaihoc.com.vn%2Fuploads%2FKHDT%2F%C4%90%C3%A0o%20t%E1%BA%A1o%2FAvatar%20Dieu%20duong%206M.JPG&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
<div class="absolute inset-0 w-full h-full bg-black/40 opacity-0 lg:group-hover:opacity-100 duration-300 ease-in-out">
</div>
</a>
<div class="md:px-1.5 w-full space-y-[8px] md:space-y-[14px] xl:space-y-[18px]">
<div class="w-full space-y-1">
<a class="title-1 max-md:!text-[14px] line-clamp-2 md:h-[54px] h-[42px] lg:group-hover:text-primary-600 duration-300 ease-in-out block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/chuong-trinh-dao-tao-thuc-hanh-lam-sang-cho-dieu-duong">
Chương trình đào tạo thực hành lâm sàng cho điều dưỡng
</a>
<div class="body-3 text-gray-700 md:line-clamp-3 max-md:hidden h-[72px] md:h-[64px]">
Bệnh viện Đại học Y Dược Thành phố Hồ Chí Minh thông báo chiêu sinh Chương trình đào tạo thực hành lâm sàng cho điều dưỡng
</div>
</div>
<div class="w-full h-px bg-gray-100">
</div>
<div class="flex justify-between items-center w-full">
<div class="flex md:gap-4 gap-2 items-center self-stretch my-auto">
<div class="flex md:gap-1.5 gap-1 items-center self-stretch my-auto">
<svg class="lucide lucide-calendar size-3.5" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2v4">
</path>
<path d="M16 2v4">
</path>
<rect height="18" rx="2" width="18" x="3" y="4">
</rect>
<path d="M3 10h18">
</path>
</svg>
<time class="self-stretch body-3 max-md:!text-[12px] my-auto">
15/10/2026
</time>
</div>
<div class="relative overflow-hidden w-9 aspect-9/5">
<img alt="icon" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/sat-image.jpeg" srcset="/_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=640&amp;q=80 640w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=750&amp;q=80 750w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=828&amp;q=80 828w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1080&amp;q=80 1080w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1200&amp;q=80 1200w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=1920&amp;q=80 1920w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=2048&amp;q=80 2048w, /_next/image?url=%2Fimages%2Fsat-image.webp&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
</div>
</div>
</div>
<a class="p-2 lg:p-3 w-full text-white bg-primary-600 rounded lg:hover:bg-primary-300 duration-300 ease-in-out block" href="https://bvdaihoc.com.vn/dao-tao/cac-lop-hoc-cap-chung-nhan-chung-chi/chuong-trinh-dao-tao-thuc-hanh-lam-sang-cho-dieu-duong">
<div class="flex items-center gap-0.5 justify-center w-full heading-4 max-md:!text-[14px]">
<span>
18.150.000
</span>
<span>
VNĐ
</span>
</div>
</a>
</article>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="xl:py-8 md:py-6 py-4">
<div class="container">
<div class="grid grid-cols-12 xl:gap-x-8 md:gap-x-6 max-md:gap-y-4">
<div class="xl:col-span-4 lg:col-span-5 col-span-full space-y-2">
<div class="display-7 text-primary-600 md:mb-3 mb-2">
Đào tạo thực hành sinh viên, học viên Đại học Y Dược TP. Hồ Chí Minh
</div>
<div class="md:mb-6 mb-4 prose prose-content">
<p>
Với chức năng là cơ sở giảng dạy thực hành, đào tạo là một trong những hoạt động nòng cốt của Bệnh viện Đại học Y Dược TP. Hồ Chí Minh, nhằm tối ưu công tác đào tạo, Bệnh viện đa dạng hóa các loại hình đào tạo để đáp ứng các yêu cầu về đào tạo của nội bộ, của các cơ sở y tế khác và của học viên nước ngoài.
</p>
</div>
<a class="btn btn-light label-2" href="https://bvdaihoc.com.vn/dao-tao-thuc-hanh-sv-hv-dhyd-tphcm">
Xem chi tiết
</a>
</div>
<div class="xl:col-span-8 lg:col-span-7 col-span-full relative rounded-lg overflow-hidden">
<div class="relative aspect-[16/9] w-full h-full">
<img alt="training" class="object-cover" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/training-1.jpeg" srcset="/_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-1.jpg&amp;w=640&amp;q=80 640w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-1.jpg&amp;w=750&amp;q=80 750w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-1.jpg&amp;w=828&amp;q=80 828w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-1.jpg&amp;w=1080&amp;q=80 1080w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-1.jpg&amp;w=1200&amp;q=80 1200w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-1.jpg&amp;w=1920&amp;q=80 1920w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-1.jpg&amp;w=2048&amp;q=80 2048w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-1.jpg&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
</div>
</div>
</div>
</section>
<section class="xl:py-8 md:py-6 py-4 bg-gray-50">
<div class="container">
<div class="display-7 text-primary-600 md:mb-3 mb-2">
Đào tạo - Chuyển giao kỹ thuật
</div>
<div class="md:mb-6 mb-4 prose prose-content">
<p>
Bệnh viện Đại học Y Dược TP. Hồ Chí Minh được Bộ Y tế giao nhiệm vụ đào tạo và chuyển giao kỹ thuật cho bệnh viện tuyến dưới theo quyết định số 2636/QĐ-BYT ngày 20/6/2017 về việc phê duyệt
<i>
Dự án Bệnh viện vệ tinh giai đoạn 2016-2020
</i>
</p>
<p>
Từ năm 2014 đến năm 2022, Bệnh viện đã đào tạo và chuyển giao một số lĩnh vực chuyên môn phục vụ công tác khám, chữa bệnh cho 06 bệnh viện trên địa bàn TP. Hồ Chí Minh và 22 bệnh viện tuyến tỉnh, 37 kỹ thuật chuyên môn đã được chuyển giao.
</p>
<p>
Các kỹ thuật chuyển giao thuộc các chuyên khoa: Mắt, Chẩn đoán Hình ảnh, Chấn thương Chỉnh hình, Lồng ngực Mạch máu, Phẫu thuật Tim mạch, Ngoại Thần kinh, Ngoại Tiêu hóa, Ngoại Gan Mật Tụy, Hậu môn Trực tràng, Tạo hình Thẩm mỹ, Tai Mũi Họng…
</p>
</div>
<a class="btn btn-light label-2" href="https://bvdaihoc.com.vn/dao-tao/dao-tao-chuyen-giao-ky-thuat">
Xem chi tiết
</a>
</div>
</section>
<section class="xl:py-8 md:py-6 py-4">
<div class="container xl:space-y-8 md:space-y-6 space-y-4">
<div class="relative aspect-[2/1] rounded-lg overflow-hidden">
<img alt="training" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/training-2.jpeg" srcset="/_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-2.jpg&amp;w=640&amp;q=80 640w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-2.jpg&amp;w=750&amp;q=80 750w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-2.jpg&amp;w=828&amp;q=80 828w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-2.jpg&amp;w=1080&amp;q=80 1080w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-2.jpg&amp;w=1200&amp;q=80 1200w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-2.jpg&amp;w=1920&amp;q=80 1920w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-2.jpg&amp;w=2048&amp;q=80 2048w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-2.jpg&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
<div class="grid grid-cols-12 xl:gap-x-8 md:gap-x-6 max-lg:gap-y-6 max-md:gap-y-4">
<div class="xl:col-span-4 lg:col-span-5 col-span-full">
<div class="display-7 text-primary-600 md:mb-3 mb-2 lg:whitespace-pre-line">
Đào tạo thực hành
lâm sàng
</div>
<div class="body-2 md:mb-6 mb-4">
<p>
Với môi trường y khoa năng động và chuyên nghiệp, được hướng dẫn thực hành trực tiếp từ Giáo sư, Bác sĩ có kinh nghiệm chuyên môn cao, người học thực hành có cơ hội được tiếp cận nhanh chóng và học hỏi các quy trình chuyên môn, giúp người học cập nhật các phương pháp và kỹ thuật mới đã và đang áp dụng trong khám, điều trị tại Bệnh viện Đại học Y Dược TP. Hồ Chí Minh.
</p>
</div>
<a class="btn btn-light label-2" href="https://bvdaihoc.com.vn/dao-tao-thuc-hanh-lam-sang">
Xem chi tiết
</a>
</div>
<div class="xl:col-span-8 lg:col-span-7 col-span-full">
<div class="relative">
<button class="btn-navigation absolute left-2 top-1/2 -translate-y-1/2 z-10 md:size-[42px] size-[32px] flex-shrink-0 items-center justify-center rounded-full btn-navigation-practical-prev md:flex hidden">
<svg class="lucide lucide-chevron-up size-4 -rotate-90" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m18 15-6-6-6 6">
</path>
</svg>
</button>
<div class="swiper swiper-initialized swiper-horizontal w-full swiper-backface-hidden">
<div class="swiper-wrapper">
<div class="swiper-slide swiper-slide-active max-w-[789px] !mr-2" data-swiper-slide-index="0" style="width: 789px; margin-right: 8px;">
<div class="relative rounded-lg aspect-[2/1] overflow-hidden">
<img alt="training 3" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/training-3.jpeg" srcset="/_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-3.jpg&amp;w=640&amp;q=80 640w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-3.jpg&amp;w=750&amp;q=80 750w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-3.jpg&amp;w=828&amp;q=80 828w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-3.jpg&amp;w=1080&amp;q=80 1080w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-3.jpg&amp;w=1200&amp;q=80 1200w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-3.jpg&amp;w=1920&amp;q=80 1920w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-3.jpg&amp;w=2048&amp;q=80 2048w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-3.jpg&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
</div>
<div class="swiper-slide swiper-slide-next max-w-[789px] !mr-2" data-swiper-slide-index="1" style="width: 789px; margin-right: 8px;">
<div class="relative rounded-lg aspect-[2/1] overflow-hidden">
<img alt="training 4" data-nimg="fill" decoding="async" loading="lazy" sizes="100vw" src="./Đào tạo tại UMC_files/training-4.jpeg" srcset="/_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-4.jpg&amp;w=640&amp;q=80 640w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-4.jpg&amp;w=750&amp;q=80 750w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-4.jpg&amp;w=828&amp;q=80 828w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-4.jpg&amp;w=1080&amp;q=80 1080w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-4.jpg&amp;w=1200&amp;q=80 1200w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-4.jpg&amp;w=1920&amp;q=80 1920w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-4.jpg&amp;w=2048&amp;q=80 2048w, /_next/image?url=%2Fimages%2Fmedical-training%2Ftraining-4.jpg&amp;w=3840&amp;q=80 3840w" style="position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent"/>
</div>
</div>
</div>
</div>
<button class="btn-navigation absolute right-2 top-1/2 -translate-y-1/2 z-10 md:size-[42px] size-[32px] flex-shrink-0 items-center justify-center rounded-full btn-navigation-practical-next md:flex hidden">
<svg class="lucide lucide-chevron-down size-4 -rotate-90" fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m6 9 6 6 6-6">
</path>
</svg>
</button>
<div class="swiper-pagination swiper-pagination-training-practical 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 md:hidden [&amp;_.swiper-pagination-bullet]:!relative [&amp;_.swiper-pagination-bullet]:before:content-[''] [&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 [&amp;_.swiper-pagination-bullet]:before:top-[-2.7px] [&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 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>
</div>
</div>
</div>
</div>
</div>
</section>
<!--$-->
<!--/$-->
<!--$-->
<!--/$-->
<script type="application/ld+json">
{"@context":"https://schema.org/","@type":"WebSite","name":"Bệnh viện Đại học Y Dược TP. Hồ Chí Minh","url":"https://bvdaihoc.com.vn","potentialAction":{"@type":"SearchAction","target":"https://bvdaihoc.com.vn/tim-kiem?q={search_term_string}","query-input":"required name=search_term_string"}}
</script>
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"Organization","@id":"https://bvdaihoc.com.vn/#organization","name":"Bệnh viện Đại học Y Dược TP. Hồ Chí Minh","alternateName":"UMC","url":"https://bvdaihoc.com.vn","logo":{"@type":"ImageObject","url":"https://bvdaihoc.com.vn/logo.png"},"sameAs":["https://facebook.com/umc.hcm","https://youtube.com/umc","https://tiktok.com/@umc"],"contactPoint":[{"@type":"ContactPoint","telephone":"+842838554269","contactType":"customer service","areaServed":"VN","availableLanguage":["Vietnamese","English"]}]}
</script>
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"Hospital","@id":"https://bvdaihoc.com.vn/#hospital","name":"Bệnh viện Đại học Y Dược TP. Hồ Chí Minh","url":"https://bvdaihoc.com.vn","logo":"https://bvdaihoc.com.vn/logo.png","image":"https://bvdaihoc.com.vn/cover.jpg","email":"bvdhyd@umc.edu.vn","telephone":"(84.28) 3855 4269","address":{"@type":"PostalAddress","streetAddress":"215 Hồng Bàng, Phường 11, Quận 5","addressLocality":"Hồ Chí Minh","addressRegion":"Hồ Chí Minh","postalCode":"70000","addressCountry":"VN"},"geo":{"@type":"GeoCoordinates","latitude":10.7553,"longitude":106.6601},"hasMap":"https://maps.app.goo.gl/EMy5LFGzG1h73oaQ6","department":[{"@type":"Hospital","name":"Cơ sở 1","url":"https://bvdaihoc.com.vn/"},{"@type":"Hospital","name":"Cơ sở 2","url":"https://bvdaihoccoso2.com.vn/"},{"@type":"Hospital","name":"Cơ sở 3","url":"https://bvdaihoccoso3.com.vn/"},{"@type":"Clinic","name":"Phòng khám Bệnh viện Đại học Y Dược","url":"https://umcclinic.com.vn/"}]}
</script>
</main>
+26
View File
@@ -0,0 +1,26 @@
import os
templates_to_update = [
'src/main/resources/templates/dao-tao-detail.html',
'src/main/resources/templates/posts/full-width.html',
'src/main/resources/templates/posts/livestream.html',
'src/main/resources/templates/posts/sidebar.html',
'src/main/resources/templates/posts/standard.html'
]
old_container = """groupHtml += '<div class="tabs-container flex justify-center xl:mb-8 md:mb-6 mb-4" style="margin-bottom: 2rem; gap: 10px; display: flex; flex-wrap: wrap;">';"""
new_container = """groupHtml += '<div class="custom-tabs-nav-container"><div class="tabs-container">';"""
for filepath in templates_to_update:
if not os.path.exists(filepath):
continue
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
content = content.replace(old_container, new_container)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Updated container in {filepath}")
+90
View File
@@ -0,0 +1,90 @@
import os
with open('src/main/resources/static/css/custom.css', 'r') as f:
css = f.read()
old_css_block = """/* CSS cho Tab Đa năng */
.custom-tab-btn {
padding: 10px 20px;
border: 1px solid #ccc;
background-color: #f8f9fa;
color: black;
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.custom-tab-btn:hover {
background-color: #e9ecef;
}
.custom-tab-btn.active {
background-color: var(--color-old-brick);
color: white;
border-color: var(--color-old-brick);
}"""
# Actually, the user manually changed it to `var(--color-old-brick);` as we see in the diff!
# So I should find the block starting with `/* CSS cho Tab Đa năng */` and ending at the end of file.
# Let's just find the index of `/* CSS cho Tab Đa năng */` and cut everything from there to the end.
idx = css.find('/* CSS cho Tab Đa năng */')
if idx != -1:
css = css[:idx]
new_css = """
/* CSS cho Tab Đa năng - Giao diện Mới (Segmented Control) */
.custom-tabs-nav-container {
display: flex;
justify-content: center;
margin-bottom: 2rem;
width: 100%;
}
.tabs-container {
background-color: #0d71ba; /* primary-600 */
border-radius: 8px; /* rounded-lg */
padding: 4px; /* p-1 */
display: flex;
gap: 4px; /* gap-x-1 */
width: 100%;
max-width: fit-content;
}
@media (max-width: 768px) {
.tabs-container {
flex-direction: column;
width: 100%;
max-width: 100%;
}
}
.custom-tab-btn {
padding: 8px 16px; /* px-4 py-2 */
border: none;
background-color: transparent;
color: white;
cursor: pointer;
border-radius: 6px; /* rounded-md */
transition: all 0.2s ease-in-out;
font-weight: 600;
font-size: 16px;
flex: 1;
text-align: center;
min-width: 120px;
}
.custom-tab-btn:hover {
color: rgba(255, 255, 255, 0.8);
background-color: transparent;
}
.custom-tab-btn.active {
background-color: white !important;
color: #0d71ba !important; /* text-primary-600 */
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
"""
with open('src/main/resources/static/css/custom.css', 'w') as f:
f.write(css + new_css)
print("Updated custom.css")
+16
View File
@@ -0,0 +1,16 @@
import os
with open('src/main/resources/static/css/custom.css', 'r') as f:
css = f.read()
# Replace max-width: fit-content with max-width: 100% or remove it
css = css.replace("max-width: fit-content;", "max-width: 860px;")
# 860px is the width in their snippet, but 100% is better.
# In their snippet they have a parent <div class="flex justify-center ..."><div class="... w-full ...">
# But the parent's width might not be constrained.
# Let's change max-width: fit-content to width: 100% and max-width: 860px;
css = css.replace("max-width: fit-content;", "max-width: 860px;")
with open('src/main/resources/static/css/custom.css', 'w') as f:
f.write(css)
print("Updated custom.css for full width")
+140
View File
@@ -0,0 +1,140 @@
const fs = require('fs');
let fileContent = fs.readFileSync('src/main/resources/templates/dao-tao-detail.html', 'utf8');
// 1. Remove the hardcoded tabs from the HTML layout
fileContent = fileContent.replace(
` <div>
<!-- Tabs -->
<div class="flex justify-center xl:mb-8 md:mb-6 mb-4" style="margin-bottom: 2rem;">
<div class="tabs-container">
<button class="dao-tao-tab-btn active" data-tab="overview"> Tổng quan </button>
<button class="dao-tao-tab-btn" data-tab="general-info"> Thông tin chung </button>
</div>
</div>
<!-- Tab: Tổng quan (nội dung chính) -->
<div id="tab-overview" class="dao-tao-tab-panel active">
<div class="prose prose-blog prose-table prose-content max-w-none" id="postContentParsed">
<!-- Nội dung chi tiết sẽ được render tại đây -->
</div>
<div id="postContentRaw" style="display:none;" th:text="\${post.content}"></div>
</div>
<!-- Tab: Thông tin chung -->
<div id="tab-general-info" class="dao-tao-tab-panel">
<div class="prose prose-blog prose-table prose-content max-w-none" id="postGeneralInfoParsed">
<!-- Thông tin chung sẽ được render tại đây -->
</div>
<div id="postExcerptRaw" style="display:none;" th:text="\${post.excerpt}"></div>
</div>
</div>`,
` <div class="prose prose-blog prose-table prose-content max-w-none" id="postContentParsed">
<!-- Nội dung chi tiết sẽ được render tại đây -->
</div>
<div id="postContentRaw" style="display:none;" th:text="\${post.content}"></div>`
);
// 2. Add the trainingCourseInfo block renderer to blocksToHtml function
const newBlockHandler = `
case "trainingCourseInfo":
var d = block.data;
var introHtml = d.introText ? d.introText.split('\\n').filter(p=>p.trim()!=='').map(p => '<p>'+p+'</p>').join('') : '';
var hoSoHtml = d.documents ? d.documents.split('\\n').filter(p=>p.trim()!=='').map(p => '<li>'+p+'</li>').join('') : '';
if(hoSoHtml) hoSoHtml = '<ul>' + hoSoHtml + '</ul>';
var luuYHtml = d.notes ? d.notes.split('\\n').filter(p=>p.trim()!=='').map(p => '<li>'+p+'</li>').join('') : '';
if(luuYHtml) luuYHtml = '<ul>' + luuYHtml + '</ul>';
var locationHtml = d.location ? d.location.split('\\n').filter(p=>p.trim()!=='').map(p => '<p>'+p+'</p>').join('') : '';
// Generating the complete Tabs HTML
html += '<div class="training-course-tabs-wrapper">';
html += ' <div class="flex justify-center xl:mb-8 md:mb-6 mb-4" style="margin-bottom: 2rem;">';
html += ' <div class="tabs-container">';
html += ' <button class="dao-tao-tab-btn active" onclick="switchTrainingTab(this, \\'overview\\')"> Tổng quan </button>';
html += ' <button class="dao-tao-tab-btn" onclick="switchTrainingTab(this, \\'general-info\\')"> Thông tin chung </button>';
html += ' </div>';
html += ' </div>';
// Tab 1
html += ' <div class="dao-tao-tab-panel active" data-tab-content="overview">';
if(introHtml) html += introHtml;
if(d.bannerText) {
html += '<div style="-webkit-text-stroke-width:0px;background-color:rgb(255, 255, 255);box-sizing:border-box;color:rgb(102, 102, 102);font-family:Roboto, sans-serif;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:center;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;"><span style="color:rgb(255,0,0);font-family:Roboto, sans-serif;font-size:12pt;"><b style="box-sizing:border-box;margin:0px;padding:0px;"><span style="box-sizing:border-box;margin:0px;padding:0px;"><strong>' + d.bannerText + '</strong></span></b></span></div>';
}
html += '<figure class="table" style="width:100%;"><table class="ck-table-resized" style="border-collapse:collapse;border-spacing:0px;width:100%;">';
html += '<colgroup><col style="width:25%;"><col style="width:75%;"></colgroup><tbody>';
if(d.time) html += '<tr><td><strong>Thời gian:</strong></td><td>' + d.time.replace(/\\n/g, '<br>') + '</td></tr>';
if(d.audience) html += '<tr><td><strong>Đối tượng đăng ký:</strong></td><td>' + d.audience.replace(/\\n/g, '<br>') + '</td></tr>';
var thongTinLopHoc = '';
if(d.fileChieuSinh) thongTinLopHoc += 'Thông báo chiêu sinh <a href="'+d.fileChieuSinh+'" target="_blank"><strong>file đính kèm</strong></a>';
if(d.filePhieuDangKy) thongTinLopHoc += (thongTinLopHoc?' và ':'') + 'phiếu đăng ký <a href="'+d.filePhieuDangKy+'" target="_blank"><strong>file đính kèm</strong></a>';
if(thongTinLopHoc) html += '<tr><td><strong>Thông tin lớp học</strong></td><td><p>' + thongTinLopHoc + '</p></td></tr>';
if(d.quantity) html += '<tr><td><strong>Số lượng:</strong></td><td><strong>' + d.quantity + '</strong></td></tr>';
if(hoSoHtml) html += '<tr><td><strong>Hồ sơ đăng ký tham dự:</strong></td><td>' + hoSoHtml + '</td></tr>';
if(luuYHtml) html += '<tr><td><strong>Lưu ý:</strong></td><td>' + luuYHtml + '</td></tr>';
html += '</tbody></table></figure>';
html += ' </div>';
// Tab 2
html += ' <div class="dao-tao-tab-panel" data-tab-content="general-info">';
html += '<figure class="table" style="width:100%;"><table style="border-collapse:collapse;border-spacing:0px;width:100%;">';
html += '<colgroup><col style="width:25%;"><col style="width:75%;"></colgroup><tbody>';
if(d.tuitionFee) html += '<tr><td><strong>Học phí:</strong></td><td><strong>' + d.tuitionFee + '</strong></td></tr>';
if(locationHtml) html += '<tr><td><strong>Địa điểm nhận hồ sơ và học phí:</strong></td><td>' + locationHtml + '</td></tr>';
var bankHtml = '';
if(d.bankAccountName) bankHtml += '<li>Tên tài khoản: ' + d.bankAccountName + '</li>';
if(d.bankAccountNumber) bankHtml += '<li>Số tài khoản: ' + d.bankAccountNumber + '</li>';
if(d.bankName) bankHtml += '<li>Tên ngân hàng: ' + d.bankName + '</li>';
if(d.transferContent) bankHtml += '<li>Nội dung: <strong>' + d.transferContent + '</strong></li>';
if(bankHtml) {
html += '<tr><td rowspan="2"><strong>Hình thức nộp học phí:</strong></td><td>&nbsp;</td></tr>';
html += '<tr><td><strong>Chuyển khoản:</strong><ul>' + bankHtml + '</ul></td></tr>';
}
html += '</tbody></table></figure>';
html += ' </div>';
html += '</div>';
break;
case "raw":`;
fileContent = fileContent.replace('case "raw":', newBlockHandler);
// 3. Remove the complicated tabSplit logic and leave just normal parsing
const tabSplitLogicRegex = /var isJsonSplit = false;[\s\S]*?if \(!isJsonSplit\) \{[\s\S]*?if \(contentText.indexOf\('<!-- TAB_SPLIT -->'\) !== -1\) \{[\s\S]*?\} else \{[\s\S]*?parseEditorContent\(contentText, 'postContentParsed'\);[\s\S]*?parseEditorContent\(excerptText, 'postGeneralInfoParsed'\);[\s\S]*?\}[\s\S]*?\}/;
fileContent = fileContent.replace(tabSplitLogicRegex, "parseEditorContent(contentText, 'postContentParsed');");
// 4. Add the switchTrainingTab function script
const scriptToAppend = `
<script>
function switchTrainingTab(btn, tabId) {
var container = btn.closest('.training-course-tabs-wrapper');
if(!container) return;
container.querySelectorAll('.dao-tao-tab-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
container.querySelectorAll('.dao-tao-tab-panel').forEach(p => p.classList.remove('active'));
var panel = container.querySelector('.dao-tao-tab-panel[data-tab-content="' + tabId + '"]');
if(panel) panel.classList.add('active');
}
</script>
`;
if (!fileContent.includes('function switchTrainingTab')) {
fileContent = fileContent.replace('</body>', scriptToAppend + '\n </body>');
}
fs.writeFileSync('src/main/resources/templates/dao-tao-detail.html', fileContent);
+139
View File
@@ -0,0 +1,139 @@
import re
with open('src/main/resources/templates/dao-tao-detail.html', 'r', encoding='utf-8') as f:
content = f.read()
# 1. Remove hardcoded tabs
old_html = """ <div>
<!-- Tabs -->
<div class="flex justify-center xl:mb-8 md:mb-6 mb-4" style="margin-bottom: 2rem;">
<div class="tabs-container">
<button class="dao-tao-tab-btn active" data-tab="overview"> Tổng quan </button>
<button class="dao-tao-tab-btn" data-tab="general-info"> Thông tin chung </button>
</div>
</div>
<!-- Tab: Tổng quan (nội dung chính) -->
<div id="tab-overview" class="dao-tao-tab-panel active">
<div class="prose prose-blog prose-table prose-content max-w-none" id="postContentParsed">
<!-- Nội dung chi tiết sẽ được render tại đây -->
</div>
<div id="postContentRaw" style="display:none;" th:text="${post.content}"></div>
</div>
<!-- Tab: Thông tin chung -->
<div id="tab-general-info" class="dao-tao-tab-panel">
<div class="prose prose-blog prose-table prose-content max-w-none" id="postGeneralInfoParsed">
<!-- Thông tin chung sẽ được render tại đây -->
</div>
<div id="postExcerptRaw" style="display:none;" th:text="${post.excerpt}"></div>
</div>
</div>"""
new_html = """ <div class="prose prose-blog prose-table prose-content max-w-none" id="postContentParsed">
<!-- Nội dung chi tiết sẽ được render tại đây -->
</div>
<div id="postContentRaw" style="display:none;" th:text="${post.content}"></div>"""
content = content.replace(old_html, new_html)
# 2. Add handler
handler = """ case "trainingCourseInfo":
var d = block.data;
var introHtml = d.introText ? d.introText.split('\\n').filter(function(p){return p.trim()!==''}).map(function(p){return '<p>'+p+'</p>'}).join('') : '';
var hoSoHtml = d.documents ? d.documents.split('\\n').filter(function(p){return p.trim()!==''}).map(function(p){return '<li>'+p+'</li>'}).join('') : '';
if(hoSoHtml) hoSoHtml = '<ul>' + hoSoHtml + '</ul>';
var luuYHtml = d.notes ? d.notes.split('\\n').filter(function(p){return p.trim()!==''}).map(function(p){return '<li>'+p+'</li>'}).join('') : '';
if(luuYHtml) luuYHtml = '<ul>' + luuYHtml + '</ul>';
var locationHtml = d.location ? d.location.split('\\n').filter(function(p){return p.trim()!==''}).map(function(p){return '<p>'+p+'</p>'}).join('') : '';
html += '<div class="training-course-tabs-wrapper">';
html += ' <div class="flex justify-center xl:mb-8 md:mb-6 mb-4" style="margin-bottom: 2rem;">';
html += ' <div class="tabs-container">';
html += ' <button class="dao-tao-tab-btn active" onclick="switchTrainingTab(this, \\'overview\\')"> Tổng quan </button>';
html += ' <button class="dao-tao-tab-btn" onclick="switchTrainingTab(this, \\'general-info\\')"> Thông tin chung </button>';
html += ' </div>';
html += ' </div>';
html += ' <div class="dao-tao-tab-panel active" data-tab-content="overview">';
if(introHtml) html += introHtml;
if(d.bannerText) {
html += '<div style="-webkit-text-stroke-width:0px;background-color:rgb(255, 255, 255);box-sizing:border-box;color:rgb(102, 102, 102);font-family:Roboto, sans-serif;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:center;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;"><span style="color:rgb(255,0,0);font-family:Roboto, sans-serif;font-size:12pt;"><b style="box-sizing:border-box;margin:0px;padding:0px;"><span style="box-sizing:border-box;margin:0px;padding:0px;"><strong>' + d.bannerText + '</strong></span></b></span></div>';
}
html += '<figure class="table" style="width:100%;"><table class="ck-table-resized" style="border-collapse:collapse;border-spacing:0px;width:100%;">';
html += '<colgroup><col style="width:25%;"><col style="width:75%;"></colgroup><tbody>';
if(d.time) html += '<tr><td><strong>Thời gian:</strong></td><td>' + d.time.replace(/\\n/g, '<br>') + '</td></tr>';
if(d.audience) html += '<tr><td><strong>Đối tượng đăng ký:</strong></td><td>' + d.audience.replace(/\\n/g, '<br>') + '</td></tr>';
var thongTinLopHoc = '';
if(d.fileChieuSinh) thongTinLopHoc += 'Thông báo chiêu sinh <a href="'+d.fileChieuSinh+'" target="_blank"><strong>file đính kèm</strong></a>';
if(d.filePhieuDangKy) thongTinLopHoc += (thongTinLopHoc?'':'') + 'phiếu đăng ký <a href="'+d.filePhieuDangKy+'" target="_blank"><strong>file đính kèm</strong></a>';
if(thongTinLopHoc) html += '<tr><td><strong>Thông tin lớp học</strong></td><td><p>' + thongTinLopHoc + '</p></td></tr>';
if(d.quantity) html += '<tr><td><strong>Số lượng:</strong></td><td><strong>' + d.quantity + '</strong></td></tr>';
if(hoSoHtml) html += '<tr><td><strong>Hồ sơ đăng ký tham dự:</strong></td><td>' + hoSoHtml + '</td></tr>';
if(luuYHtml) html += '<tr><td><strong>Lưu ý:</strong></td><td>' + luuYHtml + '</td></tr>';
html += '</tbody></table></figure>';
html += ' </div>';
html += ' <div class="dao-tao-tab-panel" data-tab-content="general-info">';
html += '<figure class="table" style="width:100%;"><table style="border-collapse:collapse;border-spacing:0px;width:100%;">';
html += '<colgroup><col style="width:25%;"><col style="width:75%;"></colgroup><tbody>';
if(d.tuitionFee) html += '<tr><td><strong>Học phí:</strong></td><td><strong>' + d.tuitionFee + '</strong></td></tr>';
if(locationHtml) html += '<tr><td><strong>Địa điểm nhận hồ sơ và học phí:</strong></td><td>' + locationHtml + '</td></tr>';
var bankHtml = '';
if(d.bankAccountName) bankHtml += '<li>Tên tài khoản: ' + d.bankAccountName + '</li>';
if(d.bankAccountNumber) bankHtml += '<li>Số tài khoản: ' + d.bankAccountNumber + '</li>';
if(d.bankName) bankHtml += '<li>Tên ngân hàng: ' + d.bankName + '</li>';
if(d.transferContent) bankHtml += '<li>Nội dung: <strong>' + d.transferContent + '</strong></li>';
if(bankHtml) {
html += '<tr><td rowspan="2"><strong>Hình thức nộp học phí:</strong></td><td>&nbsp;</td></tr>';
html += '<tr><td><strong>Chuyển khoản:</strong><ul>' + bankHtml + '</ul></td></tr>';
}
html += '</tbody></table></figure>';
html += ' </div>';
html += '</div>';
break;
case "raw":"""
content = content.replace('case "raw":', handler)
# 3. Replace complex parseEditorContent tab split logic
regex_pattern = r"var isJsonSplit = false;.*?if \(!isJsonSplit\) \{.*?if \(contentText\.indexOf\('<!-- TAB_SPLIT -->'\) !== -1\) \{.*?\} else \{.*?parseEditorContent\(contentText, 'postContentParsed'\);.*?parseEditorContent\(excerptText, 'postGeneralInfoParsed'\);.*?\}.*?\}"
content = re.sub(regex_pattern, "parseEditorContent(contentText, 'postContentParsed');", content, flags=re.DOTALL)
# 4. Add switchTrainingTab script
script = """
<script>
function switchTrainingTab(btn, tabId) {
var container = btn.closest('.training-course-tabs-wrapper');
if(!container) return;
var btns = container.querySelectorAll('.dao-tao-tab-btn');
for(var i = 0; i < btns.length; i++) btns[i].classList.remove('active');
btn.classList.add('active');
var panels = container.querySelectorAll('.dao-tao-tab-panel');
for(var j = 0; j < panels.length; j++) panels[j].classList.remove('active');
var panel = container.querySelector('.dao-tao-tab-panel[data-tab-content="' + tabId + '"]');
if(panel) panel.classList.add('active');
}
</script>
"""
if 'function switchTrainingTab' not in content:
content = content.replace('</body>', script + '\n </body>')
with open('src/main/resources/templates/dao-tao-detail.html', 'w', encoding='utf-8') as f:
f.write(content)
+189
View File
@@ -0,0 +1,189 @@
import os
import re
templates_to_update = [
'src/main/resources/templates/dao-tao-detail.html',
'src/main/resources/templates/posts/full-width.html',
'src/main/resources/templates/posts/livestream.html',
'src/main/resources/templates/posts/sidebar.html',
'src/main/resources/templates/posts/standard.html'
]
# In dao-tao-detail, the loop is `blocks.forEach(function(block) {`
# In others, the loop is `data.blocks.forEach(function(block) {`
# The switch starts with `switch(block.type) {` and ends with `}` before the end of the loop.
for filepath in templates_to_update:
if not os.path.exists(filepath):
print(f"Skipping {filepath}, does not exist")
continue
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# Find the block loop start
loop_start_match = re.search(r'(data\.blocks|blocks)\.forEach\(function\(block\)\s*\{', content)
if not loop_start_match:
print(f"Loop not found in {filepath}")
continue
# We want to replace the whole forEach logic with our new state machine logic.
# But we need the existing switch cases!
switch_match = re.search(r'switch\s*\(\s*block\.type\s*\)\s*\{([\s\S]*?)default:', content)
if not switch_match:
print(f"Switch not found in {filepath}")
continue
cases_content = switch_match.group(1)
new_parser_code = """
var inTabs = false;
var tabsGroup = null;
function renderSingleBlock(block) {
var html = "";
switch(block.type) {
""" + cases_content + """
case "tabStart":
case "tabEnd":
break;
default:
console.log("Unknown block type", block.type);
}
return html;
}
var blockList = data && data.blocks ? data.blocks : (typeof blocks !== 'undefined' ? blocks : []);
var finalHtml = "";
blockList.forEach(function(block) {
if (block.type === 'tabStart') {
if (!inTabs) {
inTabs = true;
tabsGroup = { tabs: [] };
}
tabsGroup.tabs.push({
title: block.data.title || 'Tab',
contentHtml: ''
});
return;
}
if (block.type === 'tabEnd') {
if (inTabs && tabsGroup) {
inTabs = false;
var groupHtml = '<div class="custom-tabs-wrapper">';
groupHtml += '<div class="tabs-container flex justify-center xl:mb-8 md:mb-6 mb-4" style="margin-bottom: 2rem; gap: 10px; display: flex; flex-wrap: wrap;">';
tabsGroup.tabs.forEach(function(tab, index) {
var activeClass = (index === 0) ? 'active' : '';
groupHtml += '<button class="dao-tao-tab-btn custom-tab-btn ' + activeClass + '" data-tab-idx="' + index + '" style="padding: 10px 20px; border: 1px solid #ccc; background: ' + (index===0 ? '#0055a5' : '#f8f9fa') + '; color: ' + (index===0 ? 'white' : 'black') + '; cursor: pointer;">' + tab.title + '</button>';
});
groupHtml += '</div>';
tabsGroup.tabs.forEach(function(tab, index) {
var displayStyle = (index === 0) ? 'block' : 'none';
groupHtml += '<div class="dao-tao-tab-panel custom-tab-panel" data-tab-idx="' + index + '" style="display: ' + displayStyle + ';">';
groupHtml += tab.contentHtml;
groupHtml += '</div>';
});
groupHtml += '</div>';
finalHtml += groupHtml;
tabsGroup = null;
}
return;
}
var blockHtml = renderSingleBlock(block);
if (inTabs && tabsGroup && tabsGroup.tabs.length > 0) {
tabsGroup.tabs[tabsGroup.tabs.length - 1].contentHtml += blockHtml;
} else {
finalHtml += blockHtml;
}
});
if (inTabs && tabsGroup) {
// Close unclosed tabs at the end
var groupHtml = '<div class="custom-tabs-wrapper">';
groupHtml += '<div class="tabs-container flex justify-center xl:mb-8 md:mb-6 mb-4" style="margin-bottom: 2rem; gap: 10px; display: flex; flex-wrap: wrap;">';
tabsGroup.tabs.forEach(function(tab, index) {
var activeClass = (index === 0) ? 'active' : '';
groupHtml += '<button class="dao-tao-tab-btn custom-tab-btn ' + activeClass + '" data-tab-idx="' + index + '" style="padding: 10px 20px; border: 1px solid #ccc; background: ' + (index===0 ? '#0055a5' : '#f8f9fa') + '; color: ' + (index===0 ? 'white' : 'black') + '; cursor: pointer;">' + tab.title + '</button>';
});
groupHtml += '</div>';
tabsGroup.tabs.forEach(function(tab, index) {
var displayStyle = (index === 0) ? 'block' : 'none';
groupHtml += '<div class="dao-tao-tab-panel custom-tab-panel" data-tab-idx="' + index + '" style="display: ' + displayStyle + ';">';
groupHtml += tab.contentHtml;
groupHtml += '</div>';
});
groupHtml += '</div>';
finalHtml += groupHtml;
}
html = finalHtml; // Assign back to whatever variable the original script used.
"""
# We need to replace the entire forEach loop.
# We will use regex to find the block from data.blocks.forEach up to the end of the loop.
# It's tricky to balance braces with regex. Let's do string replacement.
if filepath.endswith('dao-tao-detail.html'):
start_marker = "blocks.forEach(function(block) {"
end_marker = "html += \"</table></figure>\";\n break;"
start_idx = content.find(start_marker)
# Find the closing brace of the switch statement after end_marker
end_marker_idx = content.find(end_marker, start_idx) + len(end_marker)
# We need to replace up to the end of the forEach loop.
end_loop_idx = content.find("});", end_marker_idx) + 3
content = content[:start_idx] + new_parser_code + content[end_loop_idx:]
else:
start_marker = "data.blocks.forEach(function(block) {"
end_marker = "case \"raw\":\n html += block.data.html;\n break;"
start_idx = content.find(start_marker)
end_marker_idx = content.find(end_marker, start_idx) + len(end_marker)
end_loop_idx = content.find("});", end_marker_idx) + 3
content = content[:start_idx] + new_parser_code + content[end_loop_idx:]
# Inject tab switching script at the end of the file if not present
switch_script = """
<script>
document.addEventListener('click', function(e) {
if (e.target.classList.contains('custom-tab-btn')) {
var btn = e.target;
var container = btn.closest('.custom-tabs-wrapper');
if (!container) return;
var idx = btn.getAttribute('data-tab-idx');
container.querySelectorAll('.custom-tab-btn').forEach(function(b) {
b.classList.remove('active');
b.style.background = '#f8f9fa';
b.style.color = 'black';
});
btn.classList.add('active');
btn.style.background = '#0055a5';
btn.style.color = 'white';
container.querySelectorAll('.custom-tab-panel').forEach(function(p) {
p.style.display = 'none';
});
var activePanel = container.querySelector('.custom-tab-panel[data-tab-idx="' + idx + '"]');
if (activePanel) activePanel.style.display = 'block';
}
});
</script>
"""
if "custom-tab-btn" not in content:
content = content.replace("</body>", switch_script + "</body>")
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Updated {filepath}")
+7
View File
@@ -0,0 +1,7 @@
import re
file_path = 'src/main/resources/static/js/manage/editor-plugins/training-course.js'
with open(file_path, 'r') as f:
content = f.read()
# I will just write a new file instead of regex replacing it because it's safer.