This commit is contained in:
beabigegg
2025-07-29 20:24:40 +08:00
commit d1d68e66a7
28 changed files with 2069 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,27 @@
{% 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>
<button type="submit" class="btn btn-success">上傳並啟用</button>
<a href="{{ url_for('temp_spec.spec_list') }}" class="btn btn-secondary">取消</a>
</form>
</div>
</div>
{% endblock %}

94
templates/base.html Normal file
View File

@@ -0,0 +1,94 @@
<!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">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-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>
<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,332 @@
{% extends "base.html" %}
{% block title %}暫時規範建立 - 暫時規範系統{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.min.css" />
<link rel="stylesheet" href="https://uicdn.toast.com/tui-image-editor/latest/tui-image-editor.css" />
<style>
#tui-image-editor-container {
width: 100%;
height: 80vh; /* 增加高度佔比 */
}
.modal-dialog.modal-xl {
max-width: 98vw;
height: 90vh;
}
.modal-content {
height: 100%;
}
.modal-body {
height: 100%;
padding: 0;
overflow: hidden;
}
#tui-image-editor-container .tui-image-editor {
height: 100% !important;
}
</style>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<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-6 mb-3">
<label for="serial_number" class="form-label">暫時規範編號</label>
<input type="text" class="form-control" id="serial_number" name="serial_number" value="{{ next_spec_code }}" readonly>
</div>
<div class="col-md-6 mb-3">
<label for="theme" class="form-label">主題/目的</label>
<input type="text" class="form-control" id="theme" name="theme" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="applicant" class="form-label">申請者</label>
<input type="text" class="form-control" id="applicant" name="applicant" required>
</div>
<div class="col-md-6 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="點測"> <label>點測</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="切割"> <label>切割</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="晶粒黏著"> <label>晶粒黏著</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="銲線黏著"> <label>銲線黏著</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="錫膏焊接"> <label>錫膏焊接</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="成型"> <label>成型</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="去膠"> <label>去膠</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="吹砂"> <label>吹砂</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="電鍍"> <label>電鍍</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="切彎腳"> <label>切彎腳</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="印字"> <label>印字</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="測試"> <label>測試</label></div>
<div class="form-check form-check-inline"><input class="form-check-input" type="checkbox" name="station" value="其他" id="station_other_checkbox"> <label>其他</label></div>
</div>
<div class="mt-2" id="station_other_input_div" style="display: none;"><input type="text" class="form-control" name="station_other" placeholder="請輸入其他站別"></div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">TCCS</label>
<div class="input-group">
<select class="form-select" name="tccs_level"><option selected>請選擇 Level...</option><option value="Level 1">Level 1</option><option value="Level 2">Level 2</option><option value="Level 3">Level 3</option><option value="Level 4">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="人"> <label></label></div>
<div class="form-check form-check-inline mb-0"><input class="form-check-input" type="radio" name="tccs_4m" value="機"> <label></label></div>
<div class="form-check form-check-inline mb-0"><input class="form-check-input" type="radio" name="tccs_4m" value="料"> <label></label></div>
<div class="form-check form-check-inline mb-0"><input class="form-check-input" type="radio" name="tccs_4m" value="法"> <label></label></div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="start_date" class="form-label">實施起始日</label>
<input type="date" class="form-control" id="start_date" name="start_date" required>
</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="mb-3">
<label class="form-label">變更前內容</label>
<div id="editor-before"></div>
<textarea name="change_before" id="textarea-before" style="display: none;"></textarea>
</div>
<div class="mb-3">
<label class="form-label">變更後內容</label>
<div id="editor-after"></div>
<textarea name="change_after" id="textarea-after" style="display: none;"></textarea>
</div>
<div class="mb-3">
<label for="data_needs" class="form-label">資料收集需求</label>
<textarea class="form-control" id="data_needs" name="data_needs" rows="3" required></textarea>
</div>
<div class="d-flex justify-content-end">
<button type="button" id="preview-btn" class="btn btn-info me-2">預覽</button>
<button type="submit" class="btn btn-primary">建立暫時規範</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="modal fade" id="imageEditorModal" tabindex="-1" aria-labelledby="imageEditorModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered modal-fullscreen">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="imageEditorModalLabel">編輯圖片</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="tui-image-editor-container"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="save-image-btn">儲存變更</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script>
<script src="https://uicdn.toast.com/tui-image-editor/latest/tui-image-editor.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const imageEditorModal = new bootstrap.Modal(document.getElementById('imageEditorModal'));
const Editor = toastui.Editor;
const ImageEditor = tui.ImageEditor;
let activeEditorInstance = null;
let imageEditorInstance = null;
let targetImageElement = null;
// 上傳圖片至後端,回傳正式 URL
const uploadImage = async (blob) => {
const formData = new FormData();
formData.append('file', blob, 'edited-image.png');
try {
const response = await fetch("{{ url_for('upload.upload_image') }}", {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error(`Upload failed: ${response.statusText}`);
const result = await response.json();
return result.location;
} catch (error) {
console.error(error);
alert('圖片上傳失敗,請檢查網路或聯絡管理員。');
return null;
}
};
// 判斷圖片來源是否為 blob/base64
const isBlobOrBase64 = (url) => url.startsWith('data:') || url.startsWith('blob:');
// 啟動圖片編輯器(支援本地圖片自動轉正式 URL
const launchImageEditor = async (rawSrc) => {
let imageUrl = rawSrc;
if (isBlobOrBase64(rawSrc)) {
try {
const blob = await (await fetch(rawSrc)).blob();
const uploadedUrl = await uploadImage(blob);
if (!uploadedUrl) return;
imageUrl = uploadedUrl;
} catch (err) {
alert("圖片格式無法載入編輯器。");
return;
}
}
if (imageEditorInstance) imageEditorInstance.destroy();
imageEditorInstance = new ImageEditor('#tui-image-editor-container', {
includeUI: {
loadImage: {
path: imageUrl,
name: 'image'
},
menuBarPosition: 'bottom'
},
cssMaxWidth: 1200,
cssMaxHeight: 800,
usageStatistics: false
});
imageEditorModal.show();
};
// 建立 Markdown 編輯器並綁定圖片點擊
const createEditor = (containerId) => {
const editor = new Editor({
el: document.querySelector(containerId),
height: '300px',
initialEditType: 'wysiwyg',
previewStyle: 'vertical',
hooks: {
addImageBlobHook: async (blob, callback) => {
const newUrl = await uploadImage(blob);
if (newUrl) callback(newUrl, 'image');
}
}
});
// 綁定圖片點擊 → 進入編輯器
editor.getEditorElements().wwEditor.addEventListener('click', (event) => {
if (event.target.tagName === 'IMG') {
activeEditorInstance = editor;
targetImageElement = event.target;
launchImageEditor(event.target.src);
}
});
return editor;
};
const editorBefore = createEditor('#editor-before');
const editorAfter = createEditor('#editor-after');
// 儲存按鈕處理編輯後圖片
document.getElementById('save-image-btn').addEventListener('click', async () => {
if (!imageEditorInstance) return;
const dataURL = imageEditorInstance.toDataURL({ multiplier: 2 });
const blob = await (await fetch(dataURL)).blob();
const newUrl = await uploadImage(blob);
if (newUrl && activeEditorInstance && targetImageElement) {
targetImageElement.src = newUrl;
// 強制同步內容讓 Markdown 也更新
const updatedHtml = activeEditorInstance.getHTML();
activeEditorInstance.setHTML(updatedHtml);
}
imageEditorModal.hide();
});
// 表單送出前同步 Markdown 內容
document.getElementById('spec-form').addEventListener('submit', function () {
if (editorBefore) document.getElementById('textarea-before').value = editorBefore.getMarkdown();
if (editorAfter) document.getElementById('textarea-after').value = editorAfter.getMarkdown();
});
// 預覽產生邏輯
document.getElementById('preview-btn').addEventListener('click', async function () {
let isPreviewing = false;
if (isPreviewing) return;
isPreviewing = true;
this.disabled = true;
this.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 產生中...';
const form = document.getElementById('spec-form');
const formData = new FormData(form);
if (editorBefore) formData.set('change_before', editorBefore.getMarkdown());
if (editorAfter) formData.set('change_after', editorAfter.getMarkdown());
let data = Object.fromEntries(formData.entries());
const stations = Array.from(form.querySelectorAll('input[name="station"]:checked')).map(el => el.value);
if (stations.includes('其他') && data.station_other) {
stations[stations.indexOf('其他')] = data.station_other;
}
data.station = stations.join(', ');
data.tccs_info = data.tccs_level ? `${data.tccs_level}${data.tccs_4m ? ' (' + data.tccs_4m + ')' : ''}` : '';
try {
const response = await fetch("{{ url_for('temp_spec.preview_spec') }}", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error(`Server error: ${response.status}`);
const pdfBlob = await response.blob();
const pdfUrl = URL.createObjectURL(pdfBlob);
window.open(pdfUrl, '_blank');
} catch (error) {
console.error(error);
alert('預覽失敗,請檢查表單內容或網路。');
} finally {
isPreviewing = false;
this.disabled = false;
this.innerHTML = '預覽';
}
});
// 顯示其他站別欄位
document.getElementById('station_other_checkbox').addEventListener('change', function () {
document.getElementById('station_other_input_div').style.display = this.checked ? 'block' : 'none';
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,43 @@
{% 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 or 'danger' }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<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">
<div class="form-text">如果本次展延有新的文件版本,請在此上傳。</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 %}

40
templates/login.html Normal file
View File

@@ -0,0 +1,40 @@
{% 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="text" class="form-control" id="username" name="username" required>
</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>
</div>
{% 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 %}

143
templates/spec_list.html Normal file
View File

@@ -0,0 +1,143 @@
{% 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>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{# This block is now handled by the Toast container in base.html #}
{% endif %}
{% endwith %}
<!-- 搜尋與篩選表單 -->
<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">
<table class="table table-hover table-striped align-middle">
<thead>
<tr>
<th>編號</th>
<th>主題</th>
<th>申請者</th>
<th>建立日期</th>
<th>結束日期</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>
{% 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">
{# 只有 admin 才能看到啟用按鈕 #}
{% if current_user.role == 'admin' %}
{% if 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 %}
{% endif %}
{# editor 或 admin 都能看到展延跟終止按鈕 #}
{% if current_user.role in ['editor', 'admin'] %}
{% if 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 %}
{% endif %}
{# Admin 專屬的刪除按鈕 #}
{% 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' %}
{# 待生效狀態的下載按鈕 #}
<a href="{{ url_for('temp_spec.download_initial_pdf', spec_id=spec.id) }}" class="btn btn-sm btn-info" title="下載 PDF"><i class="bi bi-file-earmark-pdf-fill"></i></a>
{% 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 %}
{# 其他狀態(已生效、終止等),只提供已簽核的 PDF 下載 #}
<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>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- 分頁導覽 -->
<div class="card-footer bg-white">
<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,27 @@
{% 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>
<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 %}

View File

@@ -0,0 +1,91 @@
{% 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">
新增使用者
</div>
<div class="card-body">
<form action="{{ url_for('admin.create_user') }}" method="post" class="row g-3">
<div class="col-md-4">
<input type="text" name="username" class="form-control" placeholder="使用者名稱" required>
</div>
<div class="col-md-4">
<input type="password" name="password" class="form-control" placeholder="密碼" required>
</div>
<div class="col-md-2">
<select name="role" class="form-select" required>
<option value="viewer">Viewer</option>
<option value="editor">Editor</option>
<option value="admin">Admin</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-header">
現有使用者列表
</div>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>使用者名稱</th>
<th>權限</th>
<th>上次登入</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<form action="{{ url_for('admin.edit_user', user_id=user.id) }}" method="post">
<td>
<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>
</td>
<td>{{ user.last_login.strftime('%Y-%m-%d %H:%M') if user.last_login else '從未' }}</td>
<td>
<button type="submit" class="btn btn-sm btn-success">更新權限</button>
</td>
</form>
<td>
<form action="{{ url_for('admin.delete_user', user_id=user.id) }}" method="post" onsubmit="return confirm('確定要刪除這位使用者嗎?');">
<button type="submit" class="btn btn-sm btn-danger" {% if user.id == current_user.id %}disabled{% endif %}>刪除</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}