feat: 新增崗位描述與清單整合功能 v2.1

主要功能更新:
- 崗位描述保存功能:保存後資料寫入資料庫
- 崗位清單自動刷新:切換模組時自動載入最新資料
- 崗位清單檢視功能:點擊「檢視」按鈕載入對應描述
- 管理者頁面擴充:新增崗位資料管理與匯出功能
- CSV 批次匯入:支援崗位與職務資料批次匯入

後端 API 新增:
- Position Description CRUD APIs
- Position List Query & Export APIs
- CSV Template Download & Import APIs

文件更新:
- SDD.md 更新至版本 2.1
- README.md 更新功能說明與版本歷史

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-04 12:46:36 +08:00
parent d17af39bf4
commit b2584772c4
31 changed files with 6795 additions and 365 deletions

487
add_dept_function.py Normal file
View File

@@ -0,0 +1,487 @@
# -*- coding: utf-8 -*-
"""
添加部門職責頁籤和修正檢視按鈕功能
"""
import sys
import codecs
if sys.platform == 'win32':
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict')
sys.stderr = codecs.getwriter('utf-8')(sys.stderr.buffer, 'strict')
# 讀取 index.html
with open('index.html', 'r', encoding='utf-8') as f:
content = f.read()
# ==================== 1. 修正檢視按鈕功能 ====================
old_view_function = ''' // 檢視崗位
function viewPosition(code) {
const position = positionListData.find(p => p.positionCode === code);
if (position) {
showToast('檢視崗位: ' + position.positionName);
}
}'''
new_view_function = ''' // 檢視崗位 - 切換到崗位基礎資料頁籤並載入資料
function viewPosition(code) {
const position = positionListData.find(p => p.positionCode === code);
if (position) {
// 切換到崗位基礎資料模組
document.querySelectorAll('.module-btn').forEach(b => {
b.classList.remove('active', 'job-active', 'desc-active');
});
document.querySelector('.module-btn[data-module="position"]').classList.add('active');
document.querySelectorAll('.module-content').forEach(m => m.classList.remove('active'));
document.getElementById('module-position').classList.add('active');
// 填入崗位資料
document.getElementById('positionCode').value = position.positionCode || '';
document.getElementById('positionName').value = position.positionName || '';
// 根據崗位類別設定下拉選單
const categoryMap = {'技術職': '01', '管理職': '02', '業務職': '03', '行政職': '04', '專業職': '05'};
const categoryCode = categoryMap[position.positionCategory] || '';
document.getElementById('positionCategory').value = categoryCode;
if (typeof updateCategoryName === 'function') updateCategoryName();
document.getElementById('headcount').value = position.headcount || '';
document.getElementById('effectiveDate').value = position.effectiveDate || '';
// 填入組織欄位
if (document.getElementById('businessUnit')) {
document.getElementById('businessUnit').value = position.businessUnit || '';
}
if (document.getElementById('department')) {
document.getElementById('department').value = position.department || '';
}
showToast('已載入崗位: ' + position.positionName);
}
}'''
if old_view_function in content:
content = content.replace(old_view_function, new_view_function)
print("[OK] Fixed viewPosition function")
else:
print("[INFO] viewPosition function pattern not found or already updated")
# ==================== 2. 添加部門職責頁籤按鈕 ====================
# 在崗位描述按鈕後面添加部門職責按鈕
old_module_buttons = ''' <button class="module-btn" data-module="jobdesc">
<svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
崗位描述
</button>
<button class="module-btn" data-module="positionlist">'''
new_module_buttons = ''' <button class="module-btn" data-module="deptfunction">
<svg viewBox="0 0 24 24"><path d="M12 7V3H2v18h20V7H12zM6 19H4v-2h2v2zm0-4H4v-2h2v2zm0-4H4V9h2v2zm0-4H4V5h2v2zm4 12H8v-2h2v2zm0-4H8v-2h2v2zm0-4H8V9h2v2zm0-4H8V5h2v2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8v10zm-2-8h-2v2h2v-2zm0 4h-2v2h2v-2z"/></svg>
部門職責
</button>
<button class="module-btn" data-module="jobdesc">
<svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
崗位描述
</button>
<button class="module-btn" data-module="positionlist">'''
if old_module_buttons in content and 'data-module="deptfunction"' not in content:
content = content.replace(old_module_buttons, new_module_buttons)
print("[OK] Added Department Function tab button")
else:
print("[INFO] Department Function tab button already exists or pattern not found")
# ==================== 3. 添加部門職責模組內容 ====================
# 在崗位描述模組之前添加部門職責模組
dept_function_module = '''
<!-- ==================== 部門職責模組 ==================== -->
<div class="module-content" id="module-deptfunction">
<header class="app-header">
<div class="icon">
<svg viewBox="0 0 24 24"><path d="M12 7V3H2v18h20V7H12zM6 19H4v-2h2v2zm0-4H4v-2h2v2zm0-4H4V9h2v2zm0-4H4V5h2v2zm4 12H8v-2h2v2zm0-4H8v-2h2v2zm0-4H8V9h2v2zm0-4H8V5h2v2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8v10zm-2-8h-2v2h2v-2zm0 4h-2v2h2v-2z"/></svg>
</div>
<div>
<h1>部門職責維護</h1>
<div class="subtitle">Department Function Management</div>
</div>
</header>
<div class="form-card">
<form id="deptFunctionForm">
<div class="tab-content active">
<button type="button" class="ai-generate-btn" onclick="generateDeptFunction()">
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
<span>✨ I'm feeling lucky</span>
</button>
<div class="csv-buttons" style="margin-bottom: 15px;">
<button type="button" class="btn btn-secondary" onclick="importDeptFunctionCSV()">匯入 CSV</button>
<button type="button" class="btn btn-secondary" onclick="exportDeptFunctionCSV()">匯出 CSV</button>
<input type="file" id="deptFunctionCsvInput" accept=".csv" style="display: none;" onchange="handleDeptFunctionCSVImport(event)">
</div>
<div class="form-row">
<div class="form-group">
<label>部門職責編號 <span class="required">*</span></label>
<input type="text" id="deptFunctionCode" name="deptFunctionCode" required placeholder="例如: DF-001">
</div>
<div class="form-group">
<label>部門職責名稱 <span class="required">*</span></label>
<input type="text" id="deptFunctionName" name="deptFunctionName" required placeholder="例如: 軟體研發部職責">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>事業體 (Business Unit) <span class="required">*</span></label>
<select id="deptFunctionBU" name="deptFunctionBU" required>
<option value="">-- 請選擇 --</option>
<option value="SBU">SBU - 業務事業體</option>
<option value="MBU">MBU - 製造事業體</option>
<option value="HQBU">HQBU - 總部事業體</option>
<option value="ITBU">ITBU - 資訊事業體</option>
<option value="HRBU">HRBU - 人資事業體</option>
<option value="ACCBU">ACCBU - 財會事業體</option>
</select>
</div>
<div class="form-group">
<label>部門名稱 <span class="required">*</span></label>
<input type="text" id="deptFunctionDept" name="deptFunctionDept" required placeholder="例如: 軟體研發部">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>部門主管職稱</label>
<input type="text" id="deptManager" name="deptManager" placeholder="例如: 部門經理">
</div>
<div class="form-group">
<label>生效日期 <span class="required">*</span></label>
<input type="date" id="deptFunctionEffectiveDate" name="deptFunctionEffectiveDate" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>部門人數上限</label>
<input type="number" id="deptHeadcount" name="deptHeadcount" min="1" placeholder="例如: 50">
</div>
<div class="form-group">
<label>部門狀態</label>
<select id="deptStatus" name="deptStatus">
<option value="active">啟用中</option>
<option value="inactive">停用</option>
<option value="planning">規劃中</option>
</select>
</div>
</div>
<div class="form-group full-width">
<label>部門使命 (Mission)</label>
<textarea id="deptMission" name="deptMission" placeholder="• 請描述部門的核心使命..." rows="3"></textarea>
</div>
<div class="form-group full-width">
<label>部門願景 (Vision)</label>
<textarea id="deptVision" name="deptVision" placeholder="• 請描述部門的長期願景..." rows="3"></textarea>
</div>
<div class="form-group full-width">
<label>核心職責 (Core Functions) <span class="required">*</span></label>
<textarea id="deptCoreFunctions" name="deptCoreFunctions" required placeholder="• 職責一:...
• 職責二:...
• 職責三:..." rows="6"></textarea>
</div>
<div class="form-group full-width">
<label>關鍵績效指標 (KPIs)</label>
<textarea id="deptKPIs" name="deptKPIs" placeholder="• KPI 1...
• KPI 2...
• KPI 3..." rows="4"></textarea>
</div>
<div class="form-group full-width">
<label>協作部門</label>
<textarea id="deptCollaboration" name="deptCollaboration" placeholder="• 與XX部門協作進行...
• 與YY部門共同負責..." rows="3"></textarea>
</div>
<div class="form-group full-width">
<label>備注</label>
<textarea id="deptFunctionRemark" name="deptFunctionRemark" placeholder="請輸入其他補充說明..." rows="3"></textarea>
</div>
</div>
</form>
</div>
<div class="action-bar">
<div class="nav-buttons">
<button class="nav-btn" title="第一筆"><svg viewBox="0 0 24 24"><path d="M18.41 16.59L13.82 12l4.59-4.59L17 6l-6 6 6 6 1.41-1.41zM6 6h2v12H6V6z"/></svg></button>
<button class="nav-btn" title="上一筆"><svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg></button>
<button class="nav-btn" title="下一筆"><svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg></button>
<button class="nav-btn" title="最後一筆"><svg viewBox="0 0 24 24"><path d="M5.59 7.41L10.18 12l-4.59 4.59L7 18l6-6-6-6-1.41 1.41zM16 6h2v12h-2V6z"/></svg></button>
</div>
<div class="action-buttons">
<button class="btn btn-secondary" onclick="clearDeptFunctionForm()">清除</button>
<button class="btn btn-cancel" onclick="cancelDeptFunction()">取消</button>
<button class="btn btn-primary" onclick="saveDeptFunctionAndNew()">存檔續建</button>
<button class="btn btn-primary" onclick="saveDeptFunctionAndExit()">存檔離開</button>
</div>
</div>
</div>
'''
# 在崗位描述模組之前插入
jobdesc_module_start = ' <!-- ==================== 崗位描述模組 ===================='
if jobdesc_module_start in content and 'id="module-deptfunction"' not in content:
content = content.replace(jobdesc_module_start, dept_function_module + jobdesc_module_start)
print("[OK] Added Department Function module content")
else:
print("[INFO] Department Function module already exists or pattern not found")
# ==================== 4. 添加部門職責相關 JavaScript 函數 ====================
dept_function_js = '''
// ==================== 部門職責模組功能 ====================
let deptFunctionData = [
{
deptFunctionCode: 'DF-001',
deptFunctionName: '軟體研發部職責',
deptFunctionBU: 'ITBU',
deptFunctionDept: '軟體研發部',
deptManager: '研發部經理',
deptFunctionEffectiveDate: '2024-01-01',
deptHeadcount: 30,
deptStatus: 'active',
deptMission: '• 開發高品質軟體產品\\n• 持續創新技術解決方案',
deptVision: '• 成為業界領先的軟體研發團隊',
deptCoreFunctions: '• 軟體系統設計與開發\\n• 程式碼品質管理\\n• 技術架構規劃\\n• 新技術研究與導入',
deptKPIs: '• 專案準時交付率 > 90%\\n• 程式碼缺陷率 < 1%\\n• 客戶滿意度 > 4.5/5',
deptCollaboration: '• 與產品部協作需求分析\\n• 與品保部協作測試驗證',
deptFunctionRemark: ''
},
{
deptFunctionCode: 'DF-002',
deptFunctionName: '人力資源部職責',
deptFunctionBU: 'HRBU',
deptFunctionDept: '人力資源部',
deptManager: '人資部經理',
deptFunctionEffectiveDate: '2024-01-01',
deptHeadcount: 15,
deptStatus: 'active',
deptMission: '• 吸引並留住優秀人才\\n• 建立高效能組織文化',
deptVision: '• 成為最佳雇主品牌的推手',
deptCoreFunctions: '• 人才招募與甄選\\n• 員工培訓與發展\\n• 薪酬福利管理\\n• 員工關係維護',
deptKPIs: '• 人才留任率 > 85%\\n• 招募周期 < 45天\\n• 培訓滿意度 > 4.0/5',
deptCollaboration: '• 與各部門協作人力規劃\\n• 與財務部協作薪酬預算',
deptFunctionRemark: ''
}
];
function generateDeptFunction() {
const btn = event.target.closest('.ai-generate-btn');
const allFields = ['deptFunctionCode', 'deptFunctionName', 'deptFunctionBU', 'deptFunctionDept', 'deptManager', 'deptMission', 'deptVision', 'deptCoreFunctions', 'deptKPIs'];
const emptyFields = getEmptyFields(allFields);
if (emptyFields.length === 0) {
showToast('所有欄位都已填寫完成!');
return;
}
setButtonLoading(btn, true);
const existingData = {};
allFields.forEach(field => {
const value = getFieldValue(field);
if (value) existingData[field] = value;
});
const contextInfo = Object.keys(existingData).length > 0
? `\\n\\n已填寫的資料請參考這些內容來生成相關的資料\\n${JSON.stringify(existingData, null, 2)}`
: '';
const prompt = `請為HR部門職責管理系統生成部門職責資料。請用繁體中文回覆。
${contextInfo}
請「只生成」以下這些尚未填寫的欄位:${emptyFields.join(', ')}
欄位說明:
- deptFunctionCode: 部門職責編號(格式如 DF-001, DF-002
- deptFunctionName: 部門職責名稱(例如:軟體研發部職責)
- deptFunctionBU: 事業體代碼SBU/MBU/HQBU/ITBU/HRBU/ACCBU 之一)
- deptFunctionDept: 部門名稱
- deptManager: 部門主管職稱
- deptMission: 部門使命使用「•」開頭的條列式2-3項
- deptVision: 部門願景使用「•」開頭的條列式1-2項
- deptCoreFunctions: 核心職責使用「•」開頭的條列式4-6項
- deptKPIs: 關鍵績效指標使用「•」開頭的條列式3-4項
請直接返回JSON格式只包含需要生成的欄位不要有任何其他文字
{
${emptyFields.map(f => `"${f}": "..."`).join(',\\n ')}
}`;
callClaudeAPI(prompt).then(data => {
let filledCount = 0;
if (fillIfEmpty('deptFunctionCode', data.deptFunctionCode)) filledCount++;
if (fillIfEmpty('deptFunctionName', data.deptFunctionName)) filledCount++;
if (fillIfEmpty('deptFunctionBU', data.deptFunctionBU)) filledCount++;
if (fillIfEmpty('deptFunctionDept', data.deptFunctionDept)) filledCount++;
if (fillIfEmpty('deptManager', data.deptManager)) filledCount++;
if (fillIfEmpty('deptMission', data.deptMission)) filledCount++;
if (fillIfEmpty('deptVision', data.deptVision)) filledCount++;
if (fillIfEmpty('deptCoreFunctions', data.deptCoreFunctions)) filledCount++;
if (fillIfEmpty('deptKPIs', data.deptKPIs)) filledCount++;
showToast(`已自動填入 ${filledCount} 個欄位!`);
}).catch(error => {
showToast('AI 生成失敗: ' + error.message);
}).finally(() => {
setButtonLoading(btn, false);
});
}
function clearDeptFunctionForm() {
document.getElementById('deptFunctionForm').reset();
showToast('表單已清除');
}
function cancelDeptFunction() {
if (confirm('確定要取消編輯嗎?未儲存的資料將會遺失。')) {
clearDeptFunctionForm();
}
}
function saveDeptFunctionAndNew() {
if (!validateDeptFunctionForm()) return;
const formData = getDeptFunctionFormData();
deptFunctionData.push(formData);
showToast('部門職責資料已儲存!');
clearDeptFunctionForm();
// 設定新的編號
const nextCode = 'DF-' + String(deptFunctionData.length + 1).padStart(3, '0');
document.getElementById('deptFunctionCode').value = nextCode;
}
function saveDeptFunctionAndExit() {
if (!validateDeptFunctionForm()) return;
const formData = getDeptFunctionFormData();
deptFunctionData.push(formData);
showToast('部門職責資料已儲存!');
clearDeptFunctionForm();
}
function validateDeptFunctionForm() {
const required = ['deptFunctionCode', 'deptFunctionName', 'deptFunctionBU', 'deptFunctionDept', 'deptFunctionEffectiveDate', 'deptCoreFunctions'];
for (const field of required) {
const el = document.getElementById(field);
if (!el || !el.value.trim()) {
showToast('請填寫必填欄位: ' + field);
el && el.focus();
return false;
}
}
return true;
}
function getDeptFunctionFormData() {
return {
deptFunctionCode: document.getElementById('deptFunctionCode').value,
deptFunctionName: document.getElementById('deptFunctionName').value,
deptFunctionBU: document.getElementById('deptFunctionBU').value,
deptFunctionDept: document.getElementById('deptFunctionDept').value,
deptManager: document.getElementById('deptManager').value,
deptFunctionEffectiveDate: document.getElementById('deptFunctionEffectiveDate').value,
deptHeadcount: document.getElementById('deptHeadcount').value,
deptStatus: document.getElementById('deptStatus').value,
deptMission: document.getElementById('deptMission').value,
deptVision: document.getElementById('deptVision').value,
deptCoreFunctions: document.getElementById('deptCoreFunctions').value,
deptKPIs: document.getElementById('deptKPIs').value,
deptCollaboration: document.getElementById('deptCollaboration').value,
deptFunctionRemark: document.getElementById('deptFunctionRemark').value
};
}
function importDeptFunctionCSV() {
document.getElementById('deptFunctionCsvInput').click();
}
function handleDeptFunctionCSVImport(event) {
const file = event.target.files[0];
if (!file) return;
CSVUtils.importFromCSV(file, (data) => {
if (data && data.length > 0) {
const row = data[0];
Object.keys(row).forEach(key => {
const el = document.getElementById(key);
if (el) el.value = row[key];
});
showToast('已匯入 CSV 資料!');
}
});
event.target.value = '';
}
function exportDeptFunctionCSV() {
const formData = getDeptFunctionFormData();
const headers = Object.keys(formData);
CSVUtils.exportToCSV([formData], 'dept_function.csv', headers);
showToast('部門職責資料已匯出!');
}
// 獲取部門職責清單(供崗位職責選擇使用)
function getDeptFunctionList() {
return deptFunctionData.map(d => ({
code: d.deptFunctionCode,
name: d.deptFunctionName,
dept: d.deptFunctionDept,
bu: d.deptFunctionBU
}));
}
'''
# 在 usersData 定義之前插入
users_data_pattern = ' // ==================== 管理者頁面功能 ===================='
if users_data_pattern in content and 'deptFunctionData' not in content:
content = content.replace(users_data_pattern, dept_function_js + users_data_pattern)
print("[OK] Added Department Function JavaScript functions")
else:
print("[INFO] Department Function JS already exists or pattern not found")
# ==================== 5. 更新模組切換邏輯 ====================
# 找到現有的模組切換代碼並更新
old_module_switch = ''' document.querySelectorAll('.module-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.module-btn').forEach(b => {
b.classList.remove('active', 'job-active', 'desc-active');
});
btn.classList.add('active');'''
new_module_switch = ''' document.querySelectorAll('.module-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.module-btn').forEach(b => {
b.classList.remove('active', 'job-active', 'desc-active', 'dept-active');
});
btn.classList.add('active');'''
if old_module_switch in content:
content = content.replace(old_module_switch, new_module_switch)
print("[OK] Updated module switch logic")
# 寫回檔案
with open('index.html', 'w', encoding='utf-8') as f:
f.write(content)
print("\n[DONE] All modifications completed!")
print("- Fixed viewPosition button to load position data")
print("- Added Department Function tab")
print("- Added Department Function form with AI generation")