1415 lines
33 KiB
HTML
1415 lines
33 KiB
HTML
<!DOCTYPE html>
|
||
|
||
<html lang="vi">
|
||
|
||
|
||
|
||
<head>
|
||
<script>
|
||
var savedTheme = localStorage.getItem('audit_theme');
|
||
if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||
document.documentElement.setAttribute('data-theme', 'dark');
|
||
} else {
|
||
document.documentElement.setAttribute('data-theme', 'light');
|
||
}
|
||
</script>
|
||
|
||
<meta charset="UTF-8">
|
||
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
||
<link rel="icon" type="image/png" href="{{ url_for('icon', path='/icon.png') }}">
|
||
|
||
<title>Quản lý Công cụ - Admin - VNPT-MEDIA</title>
|
||
|
||
<style>
|
||
|
||
* {
|
||
|
||
margin: 0;
|
||
|
||
padding: 0;
|
||
|
||
box-sizing: border-box;
|
||
|
||
}
|
||
|
||
|
||
|
||
body {
|
||
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
|
||
background: var(--bg-main);
|
||
|
||
min-height: 100vh;
|
||
|
||
display: flex;
|
||
|
||
justify-content: center;
|
||
|
||
align-items: flex-start;
|
||
|
||
padding: 20px;
|
||
|
||
position: relative;
|
||
|
||
overflow-x: hidden;
|
||
|
||
overflow-y: auto;
|
||
|
||
color: var(--text-main);
|
||
|
||
}
|
||
|
||
|
||
|
||
body::before {
|
||
|
||
content: '';
|
||
|
||
position: absolute;
|
||
|
||
top: -50%;
|
||
|
||
left: -50%;
|
||
|
||
width: 200%;
|
||
|
||
height: 200%;
|
||
|
||
background:
|
||
|
||
linear-gradient(135deg, transparent 0%, transparent 48%, rgba(33, 150, 243, 0.05) 48%, rgba(33, 150, 243, 0.05) 52%, transparent 52%, transparent 100%),
|
||
|
||
linear-gradient(45deg, transparent 0%, transparent 48%, rgba(33, 150, 243, 0.08) 48%, rgba(33, 150, 243, 0.08) 52%, transparent 52%, transparent 100%),
|
||
|
||
linear-gradient(135deg, transparent 0%, transparent 48%, rgba(33, 150, 243, 0.03) 48%, rgba(33, 150, 243, 0.03) 52%, transparent 52%, transparent 100%);
|
||
|
||
background-size: 300px 300px, 400px 400px, 500px 500px;
|
||
|
||
background-position: 0 0, 100px 100px, 200px 0;
|
||
|
||
z-index: 0;
|
||
|
||
pointer-events: none;
|
||
|
||
}
|
||
|
||
|
||
|
||
.container {
|
||
|
||
background: var(--card-bg);
|
||
|
||
border-radius: 20px;
|
||
|
||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||
|
||
padding: 40px;
|
||
|
||
max-width: 96%;
|
||
|
||
width: 100%;
|
||
|
||
position: relative;
|
||
|
||
z-index: 1;
|
||
|
||
}
|
||
|
||
|
||
|
||
.header {
|
||
|
||
display: flex;
|
||
|
||
justify-content: space-between;
|
||
|
||
align-items: center;
|
||
|
||
margin-bottom: 30px;
|
||
|
||
padding: 20px;
|
||
|
||
background: var(--table-header-bg);
|
||
|
||
border-radius: 12px;
|
||
|
||
border: 1px solid var(--border-color);
|
||
|
||
}
|
||
|
||
|
||
|
||
.header h1 {
|
||
|
||
color: var(--primary-color);
|
||
|
||
font-size: 24px;
|
||
|
||
}
|
||
|
||
|
||
|
||
.header-actions {
|
||
|
||
display: flex;
|
||
|
||
gap: 10px;
|
||
|
||
}
|
||
|
||
|
||
|
||
.btn {
|
||
|
||
padding: 10px 20px;
|
||
|
||
border-radius: 8px;
|
||
|
||
text-decoration: none;
|
||
|
||
font-weight: 600;
|
||
|
||
transition: all 0.3s ease;
|
||
|
||
cursor: pointer;
|
||
|
||
border: none;
|
||
|
||
font-size: 14px;
|
||
|
||
}
|
||
|
||
|
||
|
||
.btn-primary {
|
||
|
||
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
|
||
|
||
color: white;
|
||
|
||
box-shadow: 0 4px 10px rgba(25, 118, 210, 0.3);
|
||
|
||
}
|
||
|
||
|
||
|
||
.btn-primary:hover {
|
||
|
||
transform: translateY(-2px);
|
||
|
||
box-shadow: 0 6px 15px rgba(25, 118, 210, 0.4);
|
||
|
||
}
|
||
|
||
|
||
|
||
.btn-secondary {
|
||
|
||
background-color: var(--border-color);
|
||
|
||
color: var(--text-main);
|
||
|
||
}
|
||
|
||
|
||
|
||
.btn-secondary:hover {
|
||
|
||
background-color: var(--border-color);
|
||
|
||
}
|
||
|
||
|
||
|
||
.btn-danger {
|
||
|
||
background: #e74c3c;
|
||
|
||
color: white;
|
||
|
||
}
|
||
|
||
|
||
|
||
.btn-danger:hover {
|
||
|
||
background: #c0392b;
|
||
|
||
}
|
||
|
||
|
||
|
||
.card {
|
||
|
||
background: var(--card-bg);
|
||
|
||
border-radius: 12px;
|
||
|
||
padding: 25px;
|
||
|
||
margin-bottom: 20px;
|
||
|
||
border: 1px solid var(--border-color);
|
||
|
||
box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.02);
|
||
|
||
}
|
||
|
||
|
||
|
||
.card-header {
|
||
|
||
display: flex;
|
||
|
||
justify-content: space-between;
|
||
|
||
align-items: center;
|
||
|
||
margin-bottom: 20px;
|
||
|
||
padding-bottom: 15px;
|
||
|
||
border-bottom: 2px solid #1976d2;
|
||
|
||
}
|
||
|
||
|
||
|
||
.card-title {
|
||
|
||
font-size: 18px;
|
||
|
||
color: var(--primary-color);
|
||
|
||
font-weight: 600;
|
||
|
||
}
|
||
|
||
|
||
|
||
.tools-table {
|
||
|
||
width: 100%;
|
||
|
||
border-collapse: collapse;
|
||
|
||
}
|
||
|
||
|
||
|
||
.tools-table th,
|
||
|
||
.tools-table td {
|
||
|
||
padding: 15px 10px;
|
||
|
||
text-align: left;
|
||
|
||
border-bottom: 1px solid var(--border-color-light);
|
||
|
||
color: var(--text-main);
|
||
|
||
}
|
||
|
||
|
||
|
||
.tools-table th {
|
||
|
||
background: rgba(25, 118, 210, 0.1);
|
||
|
||
color: var(--primary-color);
|
||
|
||
font-weight: 600;
|
||
|
||
font-size: 13px;
|
||
|
||
text-transform: uppercase;
|
||
|
||
border-bottom: 2px solid #1976d2;
|
||
|
||
}
|
||
|
||
|
||
|
||
.tools-table tr:hover {
|
||
|
||
background: var(--table-header-bg);
|
||
|
||
}
|
||
|
||
|
||
|
||
.status-badge {
|
||
|
||
display: inline-block;
|
||
|
||
padding: 5px 12px;
|
||
|
||
border-radius: 20px;
|
||
|
||
font-size: 12px;
|
||
|
||
font-weight: 600;
|
||
|
||
white-space: nowrap;
|
||
|
||
}
|
||
|
||
|
||
|
||
.status-available {
|
||
|
||
background-color: var(--success-bg);
|
||
|
||
color: var(--success-color);
|
||
|
||
}
|
||
|
||
|
||
|
||
.status-unavailable {
|
||
|
||
background-color: var(--danger-bg);
|
||
|
||
color: var(--danger-color);
|
||
|
||
}
|
||
|
||
|
||
|
||
.os-icon {
|
||
|
||
font-size: 20px;
|
||
|
||
margin-right: 8px;
|
||
|
||
}
|
||
|
||
|
||
|
||
/* Upload Form */
|
||
|
||
.upload-form {
|
||
|
||
background: var(--table-header-bg);
|
||
|
||
border: 2px dashed #1976d2;
|
||
|
||
border-radius: 15px;
|
||
|
||
padding: 30px;
|
||
|
||
text-align: center;
|
||
|
||
transition: all 0.3s ease;
|
||
|
||
}
|
||
|
||
|
||
|
||
.upload-form:hover {
|
||
|
||
background-color: var(--info-bg);
|
||
|
||
}
|
||
|
||
|
||
|
||
.upload-form.dragover {
|
||
|
||
background-color: var(--info-bg);
|
||
|
||
}
|
||
|
||
|
||
|
||
.form-group {
|
||
|
||
margin-bottom: 20px;
|
||
|
||
text-align: left;
|
||
|
||
}
|
||
|
||
|
||
|
||
.form-group label {
|
||
|
||
display: block;
|
||
|
||
margin-bottom: 8px;
|
||
|
||
color: var(--text-main);
|
||
|
||
font-size: 13px;
|
||
|
||
font-weight: 600;
|
||
|
||
}
|
||
|
||
|
||
|
||
.form-group input,
|
||
|
||
.form-group select {
|
||
|
||
width: 100%;
|
||
|
||
padding: 12px 15px;
|
||
|
||
border-radius: 8px;
|
||
|
||
border: 1px solid var(--border-color);
|
||
|
||
background: var(--card-bg);
|
||
|
||
color: var(--text-main);
|
||
|
||
font-size: 14px;
|
||
|
||
}
|
||
|
||
|
||
|
||
.form-group input:focus,
|
||
|
||
.form-group select:focus {
|
||
|
||
outline: none;
|
||
|
||
border-color: var(--primary-color);
|
||
|
||
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
|
||
|
||
}
|
||
|
||
|
||
|
||
.form-group input[type="file"] {
|
||
|
||
padding: 10px;
|
||
|
||
}
|
||
|
||
|
||
|
||
.form-row {
|
||
|
||
display: grid;
|
||
|
||
grid-template-columns: 1fr 1fr;
|
||
|
||
gap: 15px;
|
||
|
||
}
|
||
|
||
|
||
|
||
.alert {
|
||
|
||
padding: 15px 20px;
|
||
|
||
border-radius: 10px;
|
||
|
||
margin-bottom: 20px;
|
||
|
||
font-weight: 600;
|
||
|
||
}
|
||
|
||
|
||
|
||
.alert-success {
|
||
|
||
background-color: var(--success-bg);
|
||
|
||
color: var(--success-color);
|
||
|
||
border: 1px solid #c8e6c9;
|
||
|
||
}
|
||
|
||
|
||
|
||
.alert-error {
|
||
|
||
background-color: var(--danger-bg);
|
||
|
||
color: var(--danger-color);
|
||
|
||
border: 1px solid #ffcdd2;
|
||
|
||
}
|
||
|
||
|
||
|
||
.empty-state {
|
||
|
||
text-align: center;
|
||
|
||
padding: 40px;
|
||
|
||
color: var(--text-muted);
|
||
|
||
}
|
||
|
||
|
||
|
||
.empty-state-icon {
|
||
|
||
font-size: 48px;
|
||
|
||
margin-bottom: 15px;
|
||
|
||
}
|
||
|
||
|
||
|
||
@media (max-width: 768px) {
|
||
|
||
.form-row {
|
||
|
||
grid-template-columns: 1fr;
|
||
|
||
}
|
||
|
||
|
||
|
||
.header {
|
||
|
||
flex-direction: column;
|
||
|
||
gap: 15px;
|
||
|
||
}
|
||
|
||
|
||
|
||
.tools-table {
|
||
|
||
font-size: 13px;
|
||
|
||
}
|
||
|
||
|
||
|
||
.tools-table th,
|
||
|
||
.tools-table td {
|
||
|
||
padding: 10px;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
.top-nav-tabs {
|
||
|
||
display: flex;
|
||
|
||
justify-content: center;
|
||
|
||
gap: 10px;
|
||
|
||
margin-bottom: 20px;
|
||
|
||
flex-wrap: wrap;
|
||
|
||
}
|
||
|
||
.nav-tab {
|
||
|
||
padding: 10px 20px;
|
||
|
||
background: var(--bg-main);
|
||
|
||
color: var(--text-main);
|
||
|
||
text-decoration: none;
|
||
|
||
border-radius: 8px;
|
||
|
||
font-weight: 600;
|
||
|
||
font-size: 14px;
|
||
|
||
transition: all 0.3s ease;
|
||
|
||
}
|
||
|
||
.nav-tab:hover {
|
||
|
||
background-color: var(--border-color);
|
||
|
||
}
|
||
|
||
.nav-tab.active {
|
||
|
||
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
|
||
|
||
color: white;
|
||
|
||
box-shadow: 0 4px 10px rgba(25, 118, 210, 0.3);
|
||
|
||
}
|
||
|
||
|
||
|
||
</style>
|
||
|
||
</head>
|
||
|
||
|
||
|
||
<body>
|
||
|
||
{% include 'header.html' %}
|
||
|
||
<div class="container">
|
||
|
||
<!-- Top Navigation Tabs -->
|
||
<div class="top-nav-tabs"
|
||
style="display: flex; justify-content: center; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; background: rgba(25, 118, 210, 0.05); padding: 15px; border-radius: 12px; border: 1px solid rgba(25, 118, 210, 0.1);">
|
||
<a href="{{ url_for('index') }}" class="nav-tab"
|
||
style="padding: 10px 20px; background: var(--card-bg); color: var(--text-main); text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 14px; transition: all 0.3s ease; border: 1px solid var(--border-color); box-shadow: 0 2px 5px rgba(0,0,0,0.05);">🏠 Trang chủ</a>
|
||
<a href="{{ url_for('my_files') }}" class="nav-tab"
|
||
style="padding: 10px 20px; background: var(--card-bg); color: var(--text-main); text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 14px; transition: all 0.3s ease; border: 1px solid var(--border-color); box-shadow: 0 2px 5px rgba(0,0,0,0.05);">📁 File của tôi</a>
|
||
{% if user_info and user_info.role == 'admin' %}
|
||
<a href="{{ url_for('admin_tools') }}" class="nav-tab"
|
||
style="padding: 10px 20px; background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%); color: white; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 14px; transition: all 0.3s ease; box-shadow: 0 4px 10px rgba(25, 118, 210, 0.3);">⚙️ Quản trị</a>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Admin Sub Navigation Tabs -->
|
||
<div class="admin-sub-tabs"
|
||
style="display: flex; justify-content: center; gap: 8px; margin-bottom: 25px; flex-wrap: wrap;">
|
||
<a href="{{ url_for('admin_tools') }}" class="sub-tab"
|
||
style="padding: 8px 16px; background-color: var(--info-bg); color: var(--primary-color); text-decoration: none; border-radius: 20px; font-weight: 600; font-size: 13px; transition: all 0.3s ease; border: 1px solid #bbdefb;">🔧 Cài đặt chung</a>
|
||
<a href="{{ url_for('admin_users_page') }}" class="sub-tab"
|
||
style="padding: 8px 16px; background: #1565c0; color: white; text-decoration: none; border-radius: 20px; font-weight: 600; font-size: 13px; transition: all 0.3s ease; box-shadow: 0 2px 5px rgba(21, 101, 192, 0.3);">👥 Người dùng & Đơn vị</a>
|
||
<a href="{{ url_for('list_outputs') }}" class="sub-tab"
|
||
style="padding: 8px 16px; background-color: var(--info-bg); color: var(--primary-color); text-decoration: none; border-radius: 20px; font-weight: 600; font-size: 13px; transition: all 0.3s ease; border: 1px solid #bbdefb;">📂 Tất cả File</a>
|
||
<a href="{{ url_for('server_stats_page') }}" class="sub-tab"
|
||
style="padding: 8px 16px; background-color: var(--info-bg); color: var(--primary-color); text-decoration: none; border-radius: 20px; font-weight: 600; font-size: 13px; transition: all 0.3s ease; border: 1px solid #bbdefb;">📊 Thống kê</a>
|
||
</div>
|
||
|
||
|
||
|
||
{% if message %}
|
||
|
||
<div class="alert alert-{{ message.type }}">
|
||
|
||
{{ message.text }}
|
||
|
||
</div>
|
||
|
||
{% endif %}
|
||
|
||
|
||
|
||
<!-- Unit Mapping Configuration -->
|
||
|
||
<div class="card" id="unitMappingCard">
|
||
|
||
<div class="card-header">
|
||
|
||
<h2 class="card-title">🏢 Cấu hình đơn vị</h2>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<p style="color: var(--text-muted); font-size: 13px; margin-bottom: 15px;">
|
||
|
||
Gán cố định user vào đơn vị — <strong style="color: var(--danger-color);">ghi đè hoàn toàn</strong> đơn vị lấy từ LDAP/SSO.
|
||
|
||
Thay đổi có hiệu lực ngay khi lưu, không cần restart server.
|
||
|
||
</p>
|
||
|
||
|
||
|
||
<!-- Add/Edit Override Form -->
|
||
|
||
<div style="background: var(--table-header-bg); border-radius: 12px; padding: 20px; margin-bottom: 20px; border: 1px solid var(--border-color);">
|
||
|
||
<h3 style="font-size: 14px; color: var(--primary-color); margin-bottom: 12px; font-weight: 600;">➕ Thêm / Sửa gán đơn vị cố định</h3>
|
||
|
||
<div style="display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap;">
|
||
|
||
<div style="flex: 1; min-width: 200px;">
|
||
|
||
<label style="display: block; margin-bottom: 5px; color: var(--text-main); font-size: 12px; font-weight: 600;">Username</label>
|
||
|
||
<input type="text" id="overrideUsername" placeholder="vd: nguyenvana"
|
||
|
||
style="width: 100%; padding: 10px 12px; border-radius: 8px; border: 1px solid var(--border-color); font-size: 14px; background: var(--card-bg); color: var(--text-main);">
|
||
|
||
</div>
|
||
|
||
<div style="flex: 1; min-width: 200px;">
|
||
|
||
<label style="display: block; margin-bottom: 5px; color: var(--text-main); font-size: 12px; font-weight: 600;">Đơn vị</label>
|
||
|
||
<select id="overrideUnitId"
|
||
|
||
style="width: 100%; padding: 10px 12px; border-radius: 8px; border: 1px solid var(--border-color); font-size: 14px; background: var(--card-bg); color: var(--text-main);">
|
||
|
||
<option value="">-- Chọn đơn vị --</option>
|
||
|
||
{% for uid, uname in unit_mapping_units.items() %}
|
||
|
||
<option value="{{ uid }}">{{ uname }} (ID: {{ uid }})</option>
|
||
|
||
{% endfor %}
|
||
|
||
</select>
|
||
|
||
</div>
|
||
|
||
<button onclick="saveFixedOverride()" class="btn btn-primary" style="padding: 10px 24px; white-space: nowrap;">
|
||
|
||
💾 Lưu
|
||
|
||
</button>
|
||
|
||
</div>
|
||
|
||
<div id="overrideMsg" style="margin-top: 10px; font-size: 13px; display: none;"></div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<!-- Current Fixed Overrides Table -->
|
||
|
||
<h3 style="font-size: 14px; color: var(--primary-color); margin-bottom: 10px; font-weight: 600;">📋 Danh sách gán cố định hiện tại</h3>
|
||
|
||
<div style="overflow-x: auto;">
|
||
|
||
<table class="tools-table" id="overrideTable">
|
||
|
||
<thead>
|
||
|
||
<tr>
|
||
|
||
<th>Username</th>
|
||
|
||
<th>Đơn vị</th>
|
||
|
||
<th>ID</th>
|
||
|
||
<th>Thao tác</th>
|
||
|
||
</tr>
|
||
|
||
</thead>
|
||
|
||
<tbody id="overrideTableBody">
|
||
|
||
{% if fixed_unit_override and fixed_unit_override|length > 0 %}
|
||
|
||
{% for uname, uid in fixed_unit_override.items() %}
|
||
|
||
<tr id="override-row-{{ uname }}">
|
||
|
||
<td style="font-weight: 600; color: var(--text-main);">{{ uname }}</td>
|
||
|
||
<td>
|
||
|
||
<span class="status-badge" style="background: rgba(156,39,176,0.15); color: #9c27b0;">
|
||
|
||
{{ unit_mapping_units.get(uid|string, 'N/A') }}
|
||
|
||
</span>
|
||
|
||
</td>
|
||
|
||
<td style="color: var(--text-muted);">{{ uid }}</td>
|
||
|
||
<td>
|
||
|
||
<button onclick="deleteFixedOverride('{{ uname }}')" class="btn btn-danger" style="padding: 4px 12px; font-size: 12px;">
|
||
|
||
🗑️ Xóa
|
||
|
||
</button>
|
||
|
||
</td>
|
||
|
||
</tr>
|
||
|
||
{% endfor %}
|
||
|
||
{% else %}
|
||
|
||
<tr id="override-empty-row">
|
||
|
||
<td colspan="4" style="text-align: center; color: var(--text-muted); padding: 25px; font-style: italic;">
|
||
|
||
Chưa có user nào được gán cố định. Thêm ở form bên trên.
|
||
|
||
</td>
|
||
|
||
</tr>
|
||
|
||
{% endif %}
|
||
|
||
</tbody>
|
||
|
||
</table>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<!-- Group Mapping Reference (collapsed) -->
|
||
|
||
<details style="margin-top: 20px;">
|
||
|
||
<summary style="cursor: pointer; color: var(--primary-color); font-weight: 600; font-size: 14px; padding: 10px 0;">
|
||
|
||
📖 Tham khảo: Group Mapping & Users Fallback
|
||
|
||
</summary>
|
||
|
||
<div style="margin-top: 10px; display: flex; gap: 20px; flex-wrap: wrap;">
|
||
|
||
<!-- Group Mapping -->
|
||
|
||
<div style="flex: 1; min-width: 300px;">
|
||
|
||
<h4 style="font-size: 13px; color: var(--text-muted); margin-bottom: 8px;">Group Mapping (LDAP/RADIUS → Đơn vị)</h4>
|
||
|
||
<table class="tools-table" style="font-size: 13px;">
|
||
|
||
<thead>
|
||
|
||
<tr><th>Từ khóa Group</th><th>Đơn vị</th></tr>
|
||
|
||
</thead>
|
||
|
||
<tbody>
|
||
|
||
{% for kw, uid in unit_mapping_group_mapping.items() %}
|
||
|
||
<tr>
|
||
|
||
<td style="font-size: 12px;">{{ kw }}</td>
|
||
|
||
<td><span class="status-badge status-available" style="font-size: 11px;">{{ unit_mapping_units.get(uid|string, uid) }}</span></td>
|
||
|
||
</tr>
|
||
|
||
{% endfor %}
|
||
|
||
</tbody>
|
||
|
||
</table>
|
||
|
||
</div>
|
||
|
||
<!-- Users Fallback -->
|
||
|
||
<div style="flex: 1; min-width: 200px;">
|
||
|
||
<h4 style="font-size: 13px; color: var(--text-muted); margin-bottom: 8px;">Users Fallback (dự phòng cuối)</h4>
|
||
|
||
<table class="tools-table" style="font-size: 13px;">
|
||
|
||
<thead>
|
||
|
||
<tr><th>Username</th><th>Đơn vị</th></tr>
|
||
|
||
</thead>
|
||
|
||
<tbody>
|
||
|
||
{% for uname, uid in unit_mapping_users.items() %}
|
||
|
||
<tr>
|
||
|
||
<td>{{ uname }}</td>
|
||
|
||
<td><span class="status-badge" style="background: rgba(102,126,234,0.2); color: #667eea; font-size: 11px;">{{ unit_mapping_units.get(uid|string, uid) }}</span></td>
|
||
|
||
</tr>
|
||
|
||
{% endfor %}
|
||
|
||
</tbody>
|
||
|
||
</table>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p style="margin-top: 10px; font-size: 11px; color: var(--text-muted); font-style: italic;">
|
||
|
||
ℹ️ Thứ tự ưu tiên: <strong>Gán cố định</strong> → Group Mapping (LDAP) → Users Fallback
|
||
|
||
</p>
|
||
|
||
</details>
|
||
|
||
|
||
|
||
<!-- Auto Remap Existing Files -->
|
||
|
||
<div style="margin-top: 25px; padding-top: 20px; border-top: 1px dashed #ccc; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 15px;">
|
||
|
||
<div>
|
||
|
||
<h4 style="font-size: 14px; color: var(--danger-color); margin-bottom: 5px; font-weight: 600;">🔄 Quét lại dữ liệu cũ</h4>
|
||
|
||
<p style="font-size: 12px; color: var(--text-muted); margin: 0;">Tự động cập nhật lại đơn vị cho các file đã upload trong quá khứ dựa theo cấu hình gán cố định & fallback hiện tại.</p>
|
||
|
||
</div>
|
||
|
||
<button onclick="remapExisting()" class="btn btn-warning" style="padding: 8px 16px; background: #f39c12; color: white; border: none; font-weight: 600;">
|
||
|
||
🚀 Chạy quét tự động
|
||
|
||
</button>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<!-- Email Notification Settings -->
|
||
|
||
<div class="card">
|
||
|
||
<div class="card-header">
|
||
|
||
<h2 class="card-title">⚙️ Cài đặt Hệ thống</h2>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div style="display: flex; align-items: flex-start; gap: 30px; flex-wrap: wrap;">
|
||
|
||
<!-- Email Toggle Block -->
|
||
|
||
<div style="flex: 1; min-width: 280px;">
|
||
|
||
<p style="color: #a0a0a0; font-size: 13px; margin-bottom: 15px;">
|
||
|
||
Bật / Tắt chức năng gửi email thông báo khi xử lý file hoàn tất.
|
||
|
||
</p>
|
||
|
||
|
||
|
||
{% if not email_module_available %}
|
||
|
||
<!-- Module không có sẵn -->
|
||
|
||
<div style="display: flex; align-items: center; gap: 15px; padding: 18px 20px; background: rgba(231,76,60,0.12); border: 1px solid rgba(231,76,60,0.3); border-radius: 12px;">
|
||
|
||
<span style="font-size: 32px;">📧</span>
|
||
|
||
<div>
|
||
|
||
<div style="font-weight: 600; font-size: 15px; color: var(--danger-color);">Không khả dụng</div>
|
||
|
||
<div style="font-size: 12px; color: #a0a0a0; margin-top: 4px;">
|
||
|
||
Module <code style="background: rgba(255,255,255,0.1); padding: 1px 6px; border-radius: 4px;">email_notifier</code> chưa được cài đặt trên server này.
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
{% elif email_enabled %}
|
||
|
||
<!-- Module có, đang BẬT -->
|
||
|
||
<div style="display: flex; align-items: center; gap: 15px; padding: 18px 20px; background: rgba(46,204,113,0.12); border: 1px solid rgba(46,204,113,0.3); border-radius: 12px; margin-bottom: 15px;">
|
||
|
||
<span style="font-size: 32px;">✅</span>
|
||
|
||
<div>
|
||
|
||
<div style="font-weight: 600; font-size: 15px; color: #2ecc71;">Đang BẬT</div>
|
||
|
||
<div style="font-size: 12px; color: #a0a0a0; margin-top: 4px;">
|
||
|
||
Hệ thống sẽ gửi email thông báo sau mỗi lần xử lý file thành công.
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<form action="email/disable" method="post">
|
||
|
||
<button type="submit" class="btn btn-danger" style="width: 100%;">
|
||
|
||
🔕 Tắt gửi Email
|
||
|
||
</button>
|
||
|
||
</form>
|
||
|
||
|
||
|
||
{% else %}
|
||
|
||
<!-- Module có, đang TẮT -->
|
||
|
||
<div style="display: flex; align-items: center; gap: 15px; padding: 18px 20px; background: #fff; border: 1px solid var(--border-color); border-radius: 12px; margin-bottom: 15px;">
|
||
|
||
<span style="font-size: 32px;">🔕</span>
|
||
|
||
<div>
|
||
|
||
<div style="font-weight: 600; font-size: 15px; color: #a0a0a0;">Đang TẮT</div>
|
||
|
||
<div style="font-size: 12px; color: #a0a0a0; margin-top: 4px;">
|
||
|
||
Không gửi email sau khi xử lý. Nhấn bên dưới để bật.
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<form action="email/enable" method="post">
|
||
|
||
<button type="submit" class="btn btn-primary" style="width: 100%;">
|
||
|
||
📧 Bật gửi Email
|
||
|
||
</button>
|
||
|
||
</form>
|
||
|
||
{% endif %}
|
||
|
||
|
||
|
||
<p style="margin-top: 12px; font-size: 11px; color: var(--text-muted); font-style: italic;">
|
||
|
||
⚠️ Lưu ý: cài đặt này chỉ giữ trong phiên chạy hiện tại. Khi khởi động lại server, email sẽ trở về trạng thái <strong>TẮT</strong>.
|
||
|
||
</p>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<!-- User Email Stats -->
|
||
|
||
<div class="card">
|
||
|
||
<div class="card-header">
|
||
|
||
<h2 class="card-title">👥 Thống kê Người dùng (VNPT Email)</h2>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
{% if email_stats and email_stats|length > 0 %}
|
||
|
||
<div style="overflow-x: auto;">
|
||
|
||
<table class="tools-table">
|
||
|
||
<thead>
|
||
|
||
<tr>
|
||
|
||
<th>Người dùng</th>
|
||
|
||
<th>Email</th>
|
||
|
||
<th>Số lần Upload</th>
|
||
|
||
<th>Tổng File</th>
|
||
|
||
<th>Lần đầu</th>
|
||
|
||
<th>Gần nhất</th>
|
||
|
||
</tr>
|
||
|
||
</thead>
|
||
|
||
<tbody>
|
||
|
||
{% for stat in email_stats %}
|
||
|
||
<tr>
|
||
|
||
<td style="font-weight: 600; color: var(--text-main);">{{ stat.display_name }}</td>
|
||
|
||
<td style="color: var(--primary-color);">{{ stat.email }}</td>
|
||
|
||
<td><span class="status-badge" style="background: rgba(102,126,234,0.2); color: #8ba4f9;">{{ stat.total_uploads }}</span></td>
|
||
|
||
<td><span class="status-badge status-available">{{ stat.total_files }}</span></td>
|
||
|
||
<td style="font-size: 13px; color: #a0a0a0;">{{ stat.first_seen }}</td>
|
||
|
||
<td style="font-size: 13px; color: #a0a0a0;">{{ stat.last_seen }}</td>
|
||
|
||
</tr>
|
||
|
||
{% endfor %}
|
||
|
||
</tbody>
|
||
|
||
</table>
|
||
|
||
</div>
|
||
|
||
{% else %}
|
||
|
||
<div class="empty-state">
|
||
|
||
<div class="empty-state-icon">📭</div>
|
||
|
||
<p>Chưa có dữ liệu thống kê từ người dùng.</p>
|
||
|
||
</div>
|
||
|
||
{% endif %}
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<script>
|
||
// ─── Unit Mapping: Fixed Override CRUD ───────────────────────────
|
||
|
||
const UNIT_NAMES = {{ unit_mapping_units | tojson }};
|
||
const ROOT_PATH = '{{ request.scope.get("root_path", "") }}';
|
||
|
||
|
||
|
||
function showOverrideMsg(text, type) {
|
||
|
||
const el = document.getElementById('overrideMsg');
|
||
|
||
el.style.display = 'block';
|
||
|
||
el.style.color = type === 'error' ? '#c62828' : '#2e7d32';
|
||
|
||
el.style.background = type === 'error' ? '#ffebee' : '#e8f5e9';
|
||
|
||
el.style.padding = '8px 14px';
|
||
|
||
el.style.borderRadius = '8px';
|
||
|
||
el.textContent = text;
|
||
|
||
setTimeout(() => { el.style.display = 'none'; }, 4000);
|
||
|
||
}
|
||
|
||
|
||
|
||
async function saveFixedOverride() {
|
||
|
||
const username = document.getElementById('overrideUsername').value.trim().toLowerCase();
|
||
|
||
const unitId = document.getElementById('overrideUnitId').value;
|
||
|
||
|
||
|
||
if (!username) { showOverrideMsg('Vui lòng nhập username', 'error'); return; }
|
||
|
||
if (!unitId) { showOverrideMsg('Vui lòng chọn đơn vị', 'error'); return; }
|
||
|
||
|
||
|
||
try {
|
||
|
||
const resp = await fetch(ROOT_PATH + '/api/admin/unit-mapping/fixed-override', {
|
||
|
||
method: 'POST',
|
||
|
||
headers: { 'Content-Type': 'application/json' },
|
||
|
||
body: JSON.stringify({ username, unit_id: parseInt(unitId) })
|
||
|
||
});
|
||
|
||
const data = await resp.json();
|
||
|
||
if (resp.ok) {
|
||
|
||
showOverrideMsg(data.message || 'Đã lưu thành công', 'success');
|
||
|
||
// Update table dynamically
|
||
|
||
const tbody = document.getElementById('overrideTableBody');
|
||
|
||
const emptyRow = document.getElementById('override-empty-row');
|
||
|
||
if (emptyRow) emptyRow.remove();
|
||
|
||
|
||
|
||
const existingRow = document.getElementById('override-row-' + username);
|
||
|
||
if (existingRow) existingRow.remove();
|
||
|
||
|
||
|
||
const unitName = UNIT_NAMES[unitId] || 'N/A';
|
||
|
||
const tr = document.createElement('tr');
|
||
|
||
tr.id = 'override-row-' + username;
|
||
|
||
tr.innerHTML = `
|
||
|
||
<td style="font-weight: 600; color: var(--text-main);">${username}</td>
|
||
|
||
<td><span class="status-badge" style="background: rgba(156,39,176,0.15); color: #9c27b0;">${unitName}</span></td>
|
||
|
||
<td style="color: var(--text-muted);">${unitId}</td>
|
||
|
||
<td><button onclick="deleteFixedOverride('${username}')" class="btn btn-danger" style="padding: 4px 12px; font-size: 12px;">🗑️ Xóa</button></td>
|
||
|
||
`;
|
||
|
||
tbody.appendChild(tr);
|
||
|
||
// Clear form
|
||
|
||
document.getElementById('overrideUsername').value = '';
|
||
|
||
document.getElementById('overrideUnitId').value = '';
|
||
|
||
} else {
|
||
|
||
showOverrideMsg(data.detail || 'Lỗi khi lưu', 'error');
|
||
|
||
}
|
||
|
||
} catch (e) {
|
||
|
||
showOverrideMsg('Lỗi kết nối: ' + e.message, 'error');
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
async function deleteFixedOverride(username) {
|
||
|
||
if (!confirm(`Xóa gán cố định cho "${username}"?`)) return;
|
||
|
||
try {
|
||
|
||
const resp = await fetch(ROOT_PATH + `/api/admin/unit-mapping/fixed-override/${encodeURIComponent(username)}`, {
|
||
|
||
method: 'DELETE'
|
||
|
||
});
|
||
|
||
const data = await resp.json();
|
||
|
||
if (resp.ok) {
|
||
|
||
showOverrideMsg(data.message || 'Đã xóa', 'success');
|
||
|
||
const row = document.getElementById('override-row-' + username);
|
||
|
||
if (row) row.remove();
|
||
|
||
// If table is empty, show empty row
|
||
|
||
const tbody = document.getElementById('overrideTableBody');
|
||
|
||
if (tbody.children.length === 0) {
|
||
|
||
const tr = document.createElement('tr');
|
||
|
||
tr.id = 'override-empty-row';
|
||
|
||
tr.innerHTML = '<td colspan="4" style="text-align: center; color: var(--text-muted); padding: 25px; font-style: italic;">Chưa có user nào được gán cố định. Thêm ở form bên trên.</td>';
|
||
|
||
tbody.appendChild(tr);
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
showOverrideMsg(data.detail || 'Lỗi khi xóa', 'error');
|
||
|
||
}
|
||
|
||
} catch (e) {
|
||
|
||
showOverrideMsg('Lỗi kết nối: ' + e.message, 'error');
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
async function remapExisting() {
|
||
|
||
if (!confirm('Hành động này sẽ quét và cập nhật lại toàn bộ đơn vị của các file đã tải lên trước đó (dựa theo cấu hình đơn vị hiện tại). Bạn có chắc chắn muốn chạy?')) return;
|
||
|
||
try {
|
||
|
||
const resp = await fetch(ROOT_PATH + '/api/admin/unit-mapping/remap-existing', { method: 'POST' });
|
||
|
||
const data = await resp.json();
|
||
|
||
if (resp.ok) {
|
||
|
||
alert('Thành công: ' + data.message);
|
||
|
||
} else {
|
||
|
||
alert('Lỗi: ' + (data.detail || 'Không xác định'));
|
||
|
||
}
|
||
|
||
} catch (e) {
|
||
|
||
alert('Lỗi kết nối: ' + e.message);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
</script>
|
||
|
||
</body>
|
||
|
||
|
||
|
||
</html>
|
||
|