Files
hr-position-system/add_dept_function.py
DonaldFang 方士碩 b2584772c4 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>
2025-12-04 12:46:36 +08:00

488 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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")