This commit is contained in:
beabigegg
2025-09-12 08:00:56 +08:00
commit a408ce402d
54 changed files with 5626 additions and 0 deletions

12
templates/403.html Normal file
View File

@@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block title %}權限不足{% endblock %}
{% block content %}
<div class="container text-center py-5">
<h1 class="display-1">403</h1>
<h2 class="mb-4">權限不足 (Forbidden)</h2>
<p class="lead">抱歉,您沒有權限存取此頁面。</p>
<a href="{{ url_for('temp_spec.spec_list') }}" class="btn btn-primary mt-3">返回總表</a>
</div>
{% endblock %}

12
templates/404.html Normal file
View File

@@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block title %}找不到頁面{% endblock %}
{% block content %}
<div class="container text-center py-5">
<h1 class="display-1">404</h1>
<h2 class="mb-4">找不到頁面 (Not Found)</h2>
<p class="lead">抱歉,您要找的頁面不存在。</p>
<a href="{{ url_for('temp_spec.spec_list') }}" class="btn btn-primary mt-3">返回總表</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,151 @@
{% extends "base.html" %}
{% block title %}啟用暫時規範{% endblock %}
{% block content %}
<h2 class="mb-4">上傳簽核檔案以啟用規範</h2>
<div class="card">
<div class="card-header">
規範編號: <strong>{{ spec.spec_code }}</strong>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
<p><strong>主題:</strong> {{ spec.title }}</p>
<div class="alert alert-info">
請上傳已經過完整簽核的 PDF 檔案。上傳後,此規範的狀態將變為「生效」。
</div>
<div class="mb-3">
<label for="signed_file" class="form-label"><strong>已簽核的 PDF 檔案</strong></label>
<input class="form-control" type="file" id="signed_file" name="signed_file" accept=".pdf" required>
</div>
<!-- 郵件通知對象選擇 -->
<div class="mb-3">
<label for="recipients" class="form-label"><strong>郵件通知對象</strong></label>
<select id="recipients" multiple placeholder="請輸入姓名或 Email 來搜尋...">
</select>
<input type="hidden" id="recipients-hidden" name="recipients" value="" />
<div class="form-text">可搜尋姓名或 Email 地址,支援多人選擇</div>
</div>
<button type="button" class="btn btn-success" onclick="submitFormWithDebug()">上傳並啟用</button>
<a href="{{ url_for('temp_spec.spec_list') }}" class="btn btn-secondary">取消</a>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function updateHiddenField() {
if (window.recipientTomSelect) {
const selectedValues = window.recipientTomSelect.getValue();
const hiddenField = document.getElementById('recipients-hidden');
hiddenField.value = Array.isArray(selectedValues) ? selectedValues.join(',') : selectedValues;
console.log('[DEBUG] 更新隱藏字段:', hiddenField.value);
}
}
function submitFormWithDebug() {
// 確保隱藏字段有最新的值
updateHiddenField();
const recipientSelect = document.getElementById('recipients');
const hiddenField = document.getElementById('recipients-hidden');
const selectedValues = Array.from(recipientSelect.selectedOptions).map(option => option.value);
console.log('[FORM DEBUG] 表單提交時選中的收件者:', selectedValues);
console.log('[FORM DEBUG] Recipients input value:', recipientSelect.value);
console.log('[FORM DEBUG] Hidden field value:', hiddenField.value);
// 確認 Tom Select 的值
if (window.recipientTomSelect) {
console.log('[FORM DEBUG] Tom Select getValue():', window.recipientTomSelect.getValue());
}
// 發送除錯資訊到後端
const debugData = {
selectedValues: selectedValues,
recipientValue: recipientSelect.value,
hiddenFieldValue: hiddenField.value,
tomSelectValue: window.recipientTomSelect ? window.recipientTomSelect.getValue() : null
};
// 透過 fetch 發送除錯資訊
fetch('/api/debug-form', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(debugData)
}).then(() => {
console.log('[FORM DEBUG] 除錯資訊已發送,準備提交表單...');
// 延遲一點點後提交表單
setTimeout(() => {
document.querySelector('form').submit();
}, 200);
}).catch(error => {
console.error('Debug sending failed:', error);
// 即使除錯失敗也要提交表單
document.querySelector('form').submit();
});
}
document.addEventListener('DOMContentLoaded', function() {
const recipientSelect = new TomSelect('#recipients', {
valueField: 'value',
labelField: 'text',
searchField: 'text',
placeholder: '請輸入姓名或 Email 來搜尋...',
plugins: ['remove_button'],
maxItems: null,
create: false,
load: function(query, callback) {
if (!query || query.length < 2) {
callback();
return;
}
fetch(`/api/ldap-search?q=${encodeURIComponent(query)}`)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
callback(data);
})
.catch(error => {
console.error('LDAP search error:', error);
callback();
});
},
onItemAdd: function(value, item) {
// 選擇項目後清空搜尋框
this.setTextboxValue('');
this.refreshOptions(false);
// 更新隱藏字段
updateHiddenField();
},
onItemRemove: function(value) {
// 移除項目時更新隱藏字段
updateHiddenField();
},
render: {
option: function(item, escape) {
return `<div class="py-1">${escape(item.text)}</div>`;
},
item: function(item, escape) {
// 移除藍底背景,改用淺灰背景
return `<div class="badge bg-light text-dark border me-1">${escape(item.text)}</div>`;
}
}
});
// 將 Tom Select 實例儲存到全域變數以便偵錯
window.recipientTomSelect = recipientSelect;
});
</script>
{% endblock %}

98
templates/base.html Normal file
View File

@@ -0,0 +1,98 @@
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}暫時規範系統{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<!-- Toast UI Editor Core CSS -->
<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.min.css" />
<!-- Plugins CSS -->
<link rel="stylesheet" href="https://uicdn.toast.com/tui-color-picker/latest/tui-color-picker.min.css" />
<link rel="stylesheet" href="https://uicdn.toast.com/editor-plugin-color-syntax/latest/toastui-editor-plugin-color-syntax.min.css" />
<link rel="stylesheet" href="https://uicdn.toast.com/editor-plugin-table-merged-cell/latest/toastui-editor-plugin-table-merged-cell.min.css" />
<link rel="stylesheet" href="https://uicdn.toast.com/tui-image-editor/latest/tui-image-editor.min.css">
<!-- Tom Select CSS -->
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/css/tom-select.bootstrap5.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('temp_spec.spec_list') }}">暫時規範系統</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
{% if current_user.is_authenticated %}
{% if current_user.role in ['editor', 'admin'] %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('temp_spec.create_temp_spec') }}">暫時規範建立</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('temp_spec.spec_list') }}">總表檢視</a>
</li>
{% if current_user.role == 'admin' %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin.user_list') }}">權限管理</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.logout') }}">登出</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">登入</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<main class="container mt-4">
{% block content %}{% endblock %}
</main>
<!-- Toast 容器 -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<i class="bi bi-bell-fill me-2"></i>
<strong class="me-auto">通知</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
{{ message }}
</div>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Toast UI Editor Dependencies & Core -->
<script src="https://uicdn.toast.com/tui-color-picker/latest/tui-color-picker.min.js"></script>
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script>
<!-- Plugins JS -->
<script src="https://uicdn.toast.com/editor-plugin-color-syntax/latest/toastui-editor-plugin-color-syntax.min.js"></script>
<script src="https://uicdn.toast.com/editor-plugin-table-merged-cell/latest/toastui-editor-plugin-table-merged-cell.min.js"></script>
<!-- Tom Select JS -->
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js"></script>
<script>
// 啟用所有 Toast
const toastElList = document.querySelectorAll('.toast');
const toastList = [...toastElList].map(toastEl => new bootstrap.Toast(toastEl).show());
</script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,83 @@
{% extends "base.html" %}
{% block title %}建立新的暫時規範{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-10">
<h2 class="mb-4">建立新的暫時規範</h2>
<div class="card">
<div class="card-body">
<form id="spec-form" method="post">
<div class="row">
<div class="col-md-4 mb-3">
<label for="theme" class="form-label">主題/目的</label>
<input type="text" class="form-control" id="theme" name="theme" required>
</div>
<div class="col-md-4 mb-3">
<label for="applicant" class="form-label">申請者</label>
<input type="text" class="form-control" id="applicant" name="applicant" value="{{ current_user.username }}" readonly>
</div>
<div class="col-md-4 mb-3">
<label for="applicant_phone" class="form-label">電話(分機)</label>
<input type="text" class="form-control" id="applicant_phone" name="applicant_phone">
</div>
</div>
<div class="mb-3">
<label class="form-label">站別 (可多選)</label>
<div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="probing"><label>點測</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="dicing"><label>切割</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="diebond"><label>晶粒黏著</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="wirebond"><label>銲線黏著</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="solder"><label>錫膏焊接</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="molding"><label>成型</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="degate"><label>去膠</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="deflash"><label>吹砂</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="plating"><label>電鍍</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="trimform"><label>切彎腳</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="marking"><label>印字</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="tmtt"><label>測試</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="other"><label>其他</label></div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">TCCS Level</label>
<div class="input-group">
<select class="form-select" name="tccs_level">
<option selected value="">請選擇 Level...</option>
<option value="l1">Level 1</option>
<option value="l2">Level 2</option>
<option value="l3">Level 3</option>
<option value="l4">Level 4</option>
</select>
<div class="input-group-text">
<div class="form-check form-check-inline mb-0"><input class="form-check-input" type="radio" name="tccs_4m" value="man"><label></label></div>
<div class="form-check form-check-inline mb-0"><input class="form-check-input" type="radio" name="tccs_4m" value="machine"><label></label></div>
<div class="form-check form-check-inline mb-0"><input class="form-check-input" type="radio" name="tccs_4m" value="material"><label></label></div>
<div class="form-check form-check-inline mb-0"><input class="form-check-input" type="radio" name="tccs_4m" value="method"><label></label></div>
<div class="form-check form-check-inline mb-0"><input class="form-check-input" type="radio" name="tccs_4m" value="env"><label></label></div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3"><label for="package" class="form-label">Package</label><input type="text" class="form-control" id="package" name="package"></div>
<div class="col-md-4 mb-3"><label for="lot_number" class="form-label">工單批號</label><input type="text" class="form-control" id="lot_number" name="lot_number"></div>
<div class="col-md-4 mb-3"><label for="equipment_type" class="form-label">設備型(編)號</label><input type="text" class="form-control" id="equipment_type" name="equipment_type"></div>
</div>
<div class="d-flex justify-content-end">
<a href="{{ url_for('temp_spec.spec_list') }}" class="btn btn-secondary me-2">取消</a>
<button type="submit" class="btn btn-primary">建立並開始編輯</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

117
templates/extend_spec.html Normal file
View File

@@ -0,0 +1,117 @@
{% extends "base.html" %}
{% block title %}展延暫時規範{% endblock %}
{% block content %}
<h2 class="mb-4">展延暫時規範</h2>
<div class="card">
<div class="card-header">
規範編號: <strong>{{ spec.spec_code }}</strong>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
<p><strong>主題:</strong> {{ spec.title }}</p>
<p><strong>原結束日期:</strong> {{ spec.end_date.strftime('%Y-%m-%d') }}</p>
<div class="mb-3">
<label for="new_end_date" class="form-label"><strong>新的結束日期</strong></label>
<input type="date" class="form-control" id="new_end_date" name="new_end_date"
value="{{ default_new_end_date.strftime('%Y-%m-%d') }}" required>
<div class="form-text">預設為原結束日期後一個月。</div>
</div>
<div class="mb-3">
<label for="new_file" class="form-label"><strong>重新上傳佐證檔案 (必填)</strong></label>
<input class="form-control" type="file" id="new_file" name="new_file" accept=".pdf" required>
<div class="form-text">請上傳展延申請的相關佐證文件 (PDF 格式)。</div>
</div>
<!-- 郵件通知對象選擇 -->
<div class="mb-3">
<label for="recipients" class="form-label"><strong>郵件通知對象</strong></label>
{% if saved_emails %}
<div class="alert alert-info mb-2">
<small>以下為生效時設定的通知對象,您可以直接使用或進行編輯。如果修改,展延後將更新為新的通知對象。</small>
</div>
{% endif %}
<select id="recipients" name="recipients" multiple placeholder="請輸入姓名或 Email 來搜尋...">
</select>
<div class="form-text">可搜尋姓名或 Email 地址,支援多人選擇</div>
</div>
<button type="submit" class="btn btn-primary">確認展延</button>
<a href="{{ url_for('temp_spec.spec_list') }}" class="btn btn-secondary">取消</a>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// 預先載入已儲存的郵件清單
{% if saved_emails %}
const savedEmails = "{{ saved_emails }}".split(';').filter(email => email.trim());
{% else %}
const savedEmails = [];
{% endif %}
const recipientSelect = new TomSelect('#recipients', {
valueField: 'value',
labelField: 'text',
searchField: 'text',
placeholder: '請輸入姓名或 Email 來搜尋...',
plugins: ['remove_button'],
maxItems: null,
create: false,
load: function(query, callback) {
if (!query || query.length < 2) {
callback();
return;
}
fetch(`/api/ldap-search?q=${encodeURIComponent(query)}`)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
callback(data);
})
.catch(error => {
console.error('LDAP search error:', error);
callback();
});
},
onItemAdd: function(value, item) {
// 選擇項目後清空搜尋框
this.setTextboxValue('');
this.refreshOptions(false);
},
render: {
option: function(item, escape) {
return `<div class="py-1">${escape(item.text)}</div>`;
},
item: function(item, escape) {
// 移除藍底背景,改用淺灰背景
return `<div class="badge bg-light text-dark border me-1">${escape(item.text)}</div>`;
}
}
});
// 預填已儲存的郵件
if (savedEmails.length > 0) {
savedEmails.forEach(email => {
const trimmedEmail = email.trim();
if (trimmedEmail) {
recipientSelect.addOption({value: trimmedEmail, text: trimmedEmail});
recipientSelect.addItem(trimmedEmail);
}
});
}
});
</script>
{% endblock %}

41
templates/login.html Normal file
View File

@@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block title %}登入 - 暫時規範系統{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<h2 class="text-center mb-4">登入</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category or 'danger' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="card">
<div class="card-body">
<form method="post">
<div class="mb-3">
<label for="username" class="form-label">使用者帳號 (完整格式)</label>
<input type="email" class="form-control" id="username" name="username"
placeholder="請輸入完整AD帳號 (如: user@domain.com)" required>
<div class="form-text text-light fw-bold">請輸入完整的 AD 帳號格式 (包含 @domain)</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">密碼</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">登入</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block title %}編輯規範 - {{ spec.spec_code }}{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h2 class="mb-0">正在編輯: {{ spec.spec_code }}</h2>
<p class="lead text-muted">主題: {{ spec.title }}</p>
</div>
<a href="{{ url_for('temp_spec.spec_list') }}" class="btn btn-secondary"><i class="bi bi-arrow-left-circle me-2"></i>返回總表</a>
</div>
<div class="card">
<div class="card-body p-0" style="height: 85vh;">
<div id="placeholder"></div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script type="text/javascript" src="{{ onlyoffice_url }}web-apps/apps/api/documents/api.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 從後端接收的設定
const config = {{ config|tojson|safe }};
// 建立 DocEditor 物件
const docEditor = new DocsAPI.DocEditor("placeholder", config);
// 您可以在這裡加入更多事件處理,例如:
// config.events = {
// 'onAppReady': function() { console.log('Editor is ready'); },
// 'onDocumentStateChange': function(event) { console.log('Document state changed'); },
// };
});
</script>
{% endblock %}

View File

@@ -0,0 +1,34 @@
{% extends "base.html" %}
{% block title %}操作歷史 - {{ spec.spec_code }}{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="mb-0">操作歷史紀錄</h2>
<p class="lead text-muted">規範編號: {{ spec.spec_code }}</p>
</div>
<a href="{{ url_for('temp_spec.spec_list') }}" class="btn btn-secondary"><i class="bi bi-arrow-left-circle me-2"></i>返回總表</a>
</div>
<div class="card">
<div class="card-body">
<ul class="list-group list-group-flush">
{% for entry in history %}
<li class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">
<span class="badge bg-primary rounded-pill me-2">{{ entry.action }}</span>
<strong>{{ entry.user.username if entry.user else '[已刪除的使用者]' }}</strong> 執行
</h5>
<small>{{ entry.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</small>
</div>
<p class="mb-1 mt-2">{{ entry.details }}</p>
</li>
{% else %}
<li class="list-group-item">沒有任何歷史紀錄。</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}

153
templates/spec_list.html Normal file
View File

@@ -0,0 +1,153 @@
{% extends "base.html" %}
{% block title %}暫時規範總表{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">暫時規範總表</h2>
{% if current_user.role in ['editor', 'admin'] %}
<a href="{{ url_for('temp_spec.create_temp_spec') }}" class="btn btn-primary"><i class="bi bi-plus-circle-fill me-2"></i>建立新規範</a>
{% endif %}
</div>
<div class="card mb-4">
<div class="card-body">
<form method="get" action="{{ url_for('temp_spec.spec_list') }}" class="row g-3 align-items-center">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" name="query" class="form-control" placeholder="搜尋編號或主題..." value="{{ query or '' }}">
</div>
</div>
<div class="col-md-4">
<select name="status" class="form-select">
<option value="">所有狀態</option>
<option value="pending_approval" {% if status == 'pending_approval' %}selected{% endif %}>待生效</option>
<option value="active" {% if status == 'active' %}selected{% endif %}>已生效</option>
<option value="terminated" {% if status == 'terminated' %}selected{% endif %}>已終止</option>
<option value="expired" {% if status == 'expired' %}selected{% endif %}>已過期</option>
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">篩選</button>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover table-striped align-middle">
<thead>
<tr>
<th>編號</th>
<th>主題</th>
<th>申請者</th>
<th>建立日期</th>
<th>結束日期</th>
<th class="text-center">剩餘天數</th>
<th>狀態</th>
<th class="text-center">操作</th>
</tr>
</thead>
<tbody>
{% for spec in specs %}
<tr>
<td>{{ spec.spec_code }}</td>
<td>{{ spec.title }}</td>
<td>{{ spec.applicant }}</td>
<td>{{ spec.created_at.strftime('%Y-%m-%d') }}</td>
<td>{{ spec.end_date.strftime('%Y-%m-%d') }}</td>
<td class="text-center">
{% if spec.status in ['active', 'expired'] %}
{% set remaining_days = (spec.end_date - today).days %}
{% if remaining_days < 0 %}
{% set color_class = 'days-expired' %}
{% elif remaining_days <= 3 %}
{% set color_class = 'days-critical' %}
{% elif remaining_days <= 7 %}
{% set color_class = 'days-warning' %}
{% else %}
{% set color_class = 'days-safe' %}
{% endif %}
<span class="days-badge {{ color_class }}">
{{ remaining_days if remaining_days >= 0 else '已過期' }}
</span>
{% else %}
<span>-</span>
{% endif %}
</td>
<td>
{% if spec.status == 'active' %}
<span class="badge fs-6 bg-success bg-opacity-75"><i class="bi bi-check-circle-fill me-1"></i>已生效</span>
{% elif spec.status == 'pending_approval' %}
<span class="badge fs-6 bg-info bg-opacity-75 text-dark"><i class="bi bi-hourglass-split me-1"></i>待生效</span>
{% elif spec.status == 'terminated' %}
<span class="badge fs-6 bg-warning bg-opacity-75 text-dark"><i class="bi bi-slash-circle-fill me-1"></i>已終止</span>
{% else %}
<span class="badge fs-6 bg-secondary bg-opacity-75"><i class="bi bi-calendar-x-fill me-1"></i>已過期</span>
{% endif %}
</td>
<td class="text-center">
{% if spec.status == 'pending_approval' and current_user.role in ['editor', 'admin'] %}
<a href="{{ url_for('temp_spec.edit_spec', spec_id=spec.id) }}" class="btn btn-sm btn-warning" title="編輯"><i class="bi bi-pencil-fill"></i></a>
{% endif %}
{% if current_user.role == 'admin' and spec.status == 'pending_approval' %}
<a href="{{ url_for('temp_spec.activate_spec', spec_id=spec.id) }}" class="btn btn-sm btn-primary" title="啟用"><i class="bi bi-check2-circle"></i></a>
{% endif %}
{% if current_user.role in ['editor', 'admin'] and spec.status == 'active' %}
<a href="{{ url_for('temp_spec.extend_spec', spec_id=spec.id) }}" class="btn btn-sm btn-secondary" title="展延"><i class="bi bi-calendar-plus"></i></a>
<a href="{{ url_for('temp_spec.terminate_spec', spec_id=spec.id) }}" class="btn btn-sm btn-danger" title="終止"><i class="bi bi-x-circle"></i></a>
{% endif %}
{% if current_user.role == 'admin' %}
<form action="{{ url_for('temp_spec.delete_spec', spec_id=spec.id) }}" method="post" class="d-inline" onsubmit="return confirm('您確定要永久刪除這份規範及其所有相關檔案嗎?此操作無法復原。');">
<button type="submit" class="btn btn-sm btn-danger" title="永久刪除"><i class="bi bi-trash-fill"></i></button>
</form>
{% endif %}
{% if spec.status == 'pending_approval' %}
{% if current_user.role in ['editor', 'admin'] %}
<a href="{{ url_for('temp_spec.download_initial_word', spec_id=spec.id) }}" class="btn btn-sm btn-primary" title="下載 Word"><i class="bi bi-file-earmark-word-fill"></i></a>
{% endif %}
{% elif spec.uploads %}
<a href="{{ url_for('temp_spec.download_signed_pdf', spec_id=spec.id) }}" class="btn btn-sm btn-success" title="下載已簽核 PDF"><i class="bi bi-file-earmark-check-fill"></i></a>
{% endif %}
<a href="{{ url_for('temp_spec.spec_history', spec_id=spec.id) }}" class="btn btn-sm btn-outline-secondary" title="檢視歷史紀錄"><i class="bi bi-clock-history"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card-footer">
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center mb-0">
<li class="page-item {% if not pagination.has_prev %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('temp_spec.spec_list', page=pagination.prev_num, query=query, status=status) }}">上一頁</a>
</li>
{% for page_num in pagination.iter_pages() %}
{% if page_num %}
<li class="page-item {% if page_num == pagination.page %}active{% endif %}">
<a class="page-link" href="{{ url_for('temp_spec.spec_list', page=page_num, query=query, status=status) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">...</span></li>
{% endif %}
{% endfor %}
<li class="page-item {% if not pagination.has_next %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('temp_spec.spec_list', page=pagination.next_num, query=query, status=status) }}">下一頁</a>
</li>
</ul>
</nav>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,110 @@
{% extends "base.html" %}
{% block title %}提早結束暫時規範{% endblock %}
{% block content %}
<h2 class="mb-4">提早結束暫時規範</h2>
<div class="card">
<div class="card-header">
規範編號: <strong>{{ spec.spec_code }}</strong>
</div>
<div class="card-body">
<form method="post">
<p><strong>主題:</strong> {{ spec.title }}</p>
<div class="alert alert-warning">
執行此操作將會立即終止這份暫時規範,狀態將變為「已終止」,結束日期會更新為今天。
</div>
<div class="mb-3">
<label for="reason" class="form-label"><strong>提早結束原因</strong></label>
<textarea class="form-control" id="reason" name="reason" rows="4" required></textarea>
</div>
<!-- 郵件通知對象選擇 -->
<div class="mb-3">
<label for="recipients" class="form-label"><strong>郵件通知對象</strong></label>
{% if saved_emails %}
<div class="alert alert-info mb-2">
<small>以下為生效時設定的通知對象,您可以直接使用或進行編輯。</small>
</div>
{% endif %}
<select id="recipients" name="recipients" multiple placeholder="請輸入姓名或 Email 來搜尋...">
</select>
<div class="form-text">可搜尋姓名或 Email 地址,支援多人選擇</div>
</div>
<button type="submit" class="btn btn-danger">確認終止</button>
<a href="{{ url_for('temp_spec.spec_list') }}" class="btn btn-secondary">取消</a>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// 預先載入已儲存的郵件清單
{% if saved_emails %}
const savedEmails = "{{ saved_emails }}".split(';').filter(email => email.trim());
{% else %}
const savedEmails = [];
{% endif %}
const recipientSelect = new TomSelect('#recipients', {
valueField: 'value',
labelField: 'text',
searchField: 'text',
placeholder: '請輸入姓名或 Email 來搜尋...',
plugins: ['remove_button'],
maxItems: null,
create: false,
load: function(query, callback) {
if (!query || query.length < 2) {
callback();
return;
}
fetch(`/api/ldap-search?q=${encodeURIComponent(query)}`)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
callback(data);
})
.catch(error => {
console.error('LDAP search error:', error);
callback();
});
},
onItemAdd: function(value, item) {
// 選擇項目後清空搜尋框
this.setTextboxValue('');
this.refreshOptions(false);
},
render: {
option: function(item, escape) {
return `<div class="py-1">${escape(item.text)}</div>`;
},
item: function(item, escape) {
// 移除藍底背景,改用淺灰背景
return `<div class="badge bg-light text-dark border me-1">${escape(item.text)}</div>`;
}
}
});
// 預填已儲存的郵件
if (savedEmails.length > 0) {
savedEmails.forEach(email => {
const trimmedEmail = email.trim();
if (trimmedEmail) {
recipientSelect.addOption({value: trimmedEmail, text: trimmedEmail});
recipientSelect.addItem(trimmedEmail);
}
});
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,234 @@
{% extends "base.html" %}
{% block title %}權限管理{% endblock %}
{% block content %}
<h2 class="mb-4">權限管理</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- 設定新管理員 -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<i class="bi bi-person-plus-fill"></i> 設定管理員權限
</div>
<div class="card-body">
<form action="{{ url_for('admin.set_admin') }}" method="post" class="row g-3">
<div class="col-md-8">
<label for="username" class="form-label">AD 帳號</label>
<input type="text" name="username" class="form-control" id="username"
placeholder="例如user@panjit.com.tw 或 username" required>
<div class="form-text">輸入需要設定為管理員的 AD 帳號</div>
</div>
<div class="col-md-4 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-shield-check"></i> 設定為管理員
</button>
</div>
</form>
</div>
</div>
<!-- 現有使用者權限管理 -->
<div class="card">
<div class="card-header bg-secondary text-white">
<i class="bi bi-people-fill"></i> 現有使用者權限管理
</div>
<div class="card-body">
{% if users %}
<div class="table-responsive">
<table class="table table-striped table-hover align-middle">
<thead>
<tr>
<th>ID</th>
<th>AD 帳號</th>
<th>目前權限</th>
<th>上次登入</th>
<th>權限管理</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr {% if user.id == current_user.id %}class="table-warning"{% endif %}>
<td>{{ user.id }}</td>
<td>
{{ user.username }}
{% if user.id == current_user.id %}
<span class="badge bg-info ms-1">目前使用者</span>
{% endif %}
</td>
<td>
<span class="badge
{% if user.role == 'admin' %}bg-danger
{% elif user.role == 'editor' %}bg-warning text-dark
{% else %}bg-secondary
{% endif %}">
{% if user.role == 'admin' %}
<i class="bi bi-shield-fill"></i> 管理員
{% elif user.role == 'editor' %}
<i class="bi bi-pencil-fill"></i> 編輯者
{% else %}
<i class="bi bi-eye-fill"></i> 檢視者
{% endif %}
</span>
</td>
<td>
{% if user.last_login %}
{{ user.last_login.strftime('%Y-%m-%d %H:%M') }}
{% else %}
<span class="text-muted">從未登入</span>
{% endif %}
</td>
<td>
<form action="{{ url_for('admin.edit_user_role', user_id=user.id) }}"
method="post" class="d-inline">
<div class="input-group input-group-sm">
<select name="role" class="form-select form-select-sm">
<option value="viewer" {% if user.role == 'viewer' %}selected{% endif %}>
檢視者 (Viewer)
</option>
<option value="editor" {% if user.role == 'editor' %}selected{% endif %}>
編輯者 (Editor)
</option>
<option value="admin" {% if user.role == 'admin' %}selected{% endif %}>
管理員 (Admin)
</option>
</select>
<button type="submit" class="btn btn-outline-primary btn-sm">
<i class="bi bi-check-lg"></i>
</button>
</div>
</form>
</td>
<td>
{% if user.id != current_user.id %}
<form action="{{ url_for('admin.delete_user', user_id=user.id) }}"
method="post"
onsubmit="return confirm('確定要刪除使用者 {{ user.username }} 嗎?此操作無法復原!');"
class="d-inline">
<button type="submit" class="btn btn-outline-danger btn-sm">
<i class="bi bi-trash"></i>
</button>
</form>
{% else %}
<button type="button" class="btn btn-outline-secondary btn-sm" disabled
title="無法刪除自己的帳號">
<i class="bi bi-shield-x"></i>
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
目前沒有任何使用者記錄。使用者會在首次透過 AD 登入時自動建立。
</div>
{% endif %}
</div>
</div>
<!-- 權限說明 -->
<div class="card mt-4">
<div class="card-header">
<i class="bi bi-info-circle"></i> 權限等級說明
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<h6><span class="badge bg-secondary"><i class="bi bi-eye-fill"></i> 檢視者 (Viewer)</span></h6>
<ul class="small">
<li>登入系統</li>
<li>檢視規範列表</li>
<li>下載已生效的 PDF 文件</li>
<li>查看歷史記錄</li>
</ul>
</div>
<div class="col-md-4">
<h6><span class="badge bg-warning text-dark"><i class="bi bi-pencil-fill"></i> 編輯者 (Editor)</span></h6>
<ul class="small">
<li>包含 Viewer 所有權限</li>
<li>建立新的暫時規範</li>
<li>編輯規範內容</li>
<li>展延規範</li>
<li>終止規範</li>
<li>下載 Word 編輯檔案</li>
</ul>
</div>
<div class="col-md-4">
<h6><span class="badge bg-danger"><i class="bi bi-shield-fill"></i> 管理員 (Admin)</span></h6>
<ul class="small">
<li>包含 Editor 所有權限</li>
<li>啟用待生效的規範</li>
<li>管理使用者權限</li>
<li>刪除規範</li>
<li>系統設定管理</li>
</ul>
</div>
</div>
</div>
</div>
<!-- LDAP 整合說明 -->
<div class="card mt-4">
<div class="card-header bg-info text-white">
<i class="bi bi-diagram-3"></i> LDAP/AD 整合說明
</div>
<div class="card-body">
<p><strong>系統運作方式:</strong></p>
<ol>
<li>使用者首次使用 AD 帳號登入時,系統會自動建立本地使用者記錄,預設權限為 <code>viewer</code></li>
<li>管理員可以在此頁面調整使用者權限等級</li>
<li>使用者的身份認證完全由 Active Directory 處理,本系統不儲存密碼</li>
<li>刪除本地使用者記錄不會影響 AD 帳號,使用者仍可重新登入(但會重置為 viewer 權限)</li>
</ol>
<div class="alert alert-warning mt-3">
<i class="bi bi-exclamation-triangle"></i>
<strong>重要提醒:</strong>確保至少保留一個管理員帳號,避免無法進行權限管理。
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// 自動提交表單當選擇權限改變時的確認
document.addEventListener('DOMContentLoaded', function() {
const roleSelects = document.querySelectorAll('select[name="role"]');
roleSelects.forEach(select => {
select.addEventListener('change', function() {
const form = this.closest('form');
const username = form.closest('tr').querySelector('td:nth-child(2)').textContent.trim();
const newRole = this.value;
if (confirm(`確定要將使用者 "${username}" 的權限變更為 "${newRole}" 嗎?`)) {
// 使用者確認後自動提交
setTimeout(() => {
form.submit();
}, 100);
} else {
// 使用者取消,恢復原來的選擇
this.selectedIndex = this.getAttribute('data-original-index') || 0;
}
});
// 記錄原始選擇索引
select.setAttribute('data-original-index', select.selectedIndex);
});
});
</script>
{% endblock %}