backup: 完成 HR_position_ 表格前綴重命名與欄位對照表整理

變更內容:
- 所有資料表加上 HR_position_ 前綴
- 整理完整欄位顯示名稱與 ID 對照表
- 模組化 JS 檔案 (admin.js, ai.js, csv.js 等)
- 專案結構優化 (docs/, scripts/, tests/ 等)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-09 12:05:20 +08:00
parent a068ef9704
commit a6af297623
82 changed files with 8685 additions and 4933 deletions

281
scripts/add_csv_buttons.py Normal file
View File

@@ -0,0 +1,281 @@
"""
為每個模組加入 CSV 匯入匯出按鈕
"""
import sys
import codecs
# 設置 UTF-8 編碼Windows 編碼修正)
if sys.platform == 'win32':
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict')
sys.stderr = codecs.getwriter('utf-8')(sys.stderr.buffer, 'strict')
with open('index.html', 'r', encoding='utf-8') as f:
content = f.read()
# 備份
with open('index.html.backup_csv', 'w', encoding='utf-8') as f:
f.write(content)
# 1. 在 <head> 中加入 csv_utils.js
if '<script src="csv_utils.js"></script>' not in content:
head_insertion = ' <script src="csv_utils.js"></script>\n</head>'
content = content.replace('</head>', head_insertion)
print("[OK] Added csv_utils.js reference")
# 2. 為崗位資料模組加入 CSV 按鈕
# 在 action-buttons 區域前加入 CSV 按鈕
position_csv_buttons = ''' <!-- CSV 匯入匯出按鈕 -->
<div class="csv-buttons" style="margin-bottom: 15px; display: flex; gap: 10px;">
<button type="button" class="btn btn-secondary" onclick="exportPositionsCSV()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>
匯出 CSV
</button>
<button type="button" class="btn btn-secondary" onclick="importPositionsCSV()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z"/></svg>
匯入 CSV
</button>
<input type="file" id="positionCSVInput" accept=".csv" style="display: none;" onchange="handlePositionCSVImport(event)">
</div>
'''
# 找到崗位資料模組的 action-buttons
old_position_section = ''' <div class="action-buttons">
<button type="button" class="btn btn-primary" onclick="savePositionAndExit()">'''
new_position_section = position_csv_buttons + ''' <div class="action-buttons">
<button type="button" class="btn btn-primary" onclick="savePositionAndExit()">'''
if old_position_section in content and position_csv_buttons not in content:
content = content.replace(old_position_section, new_position_section)
print("[OK] Added CSV buttons to Position module")
# 3. 為職務資料模組加入 CSV 按鈕
job_csv_buttons = ''' <!-- CSV 匯入匯出按鈕 -->
<div class="csv-buttons" style="margin-bottom: 15px; display: flex; gap: 10px;">
<button type="button" class="btn btn-secondary" onclick="exportJobsCSV()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>
匯出 CSV
</button>
<button type="button" class="btn btn-secondary" onclick="importJobsCSV()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z"/></svg>
匯入 CSV
</button>
<input type="file" id="jobCSVInput" accept=".csv" style="display: none;" onchange="handleJobCSVImport(event)">
</div>
'''
# 找到職務資料模組的 action-buttons注意有不同的函數名
old_job_section = ''' <div class="action-buttons">
<button type="button" class="btn btn-primary" onclick="saveJobAndExit()">'''
new_job_section = job_csv_buttons + ''' <div class="action-buttons">
<button type="button" class="btn btn-primary" onclick="saveJobAndExit()">'''
if old_job_section in content and job_csv_buttons not in content:
content = content.replace(old_job_section, new_job_section)
print("[OK] Added CSV buttons to Job module")
# 4. 為崗位描述模組加入 CSV 按鈕
desc_csv_buttons = ''' <!-- CSV 匯入匯出按鈕 -->
<div class="csv-buttons" style="margin-bottom: 15px; display: flex; gap: 10px;">
<button type="button" class="btn btn-secondary" onclick="exportDescriptionsCSV()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>
匯出 CSV
</button>
<button type="button" class="btn btn-secondary" onclick="importDescriptionsCSV()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z"/></svg>
匯入 CSV
</button>
<input type="file" id="descCSVInput" accept=".csv" style="display: none;" onchange="handleDescCSVImport(event)">
</div>
'''
# 找到崗位描述模組的 action-buttons
old_desc_section = ''' <div class="action-buttons">
<button type="button" class="btn btn-primary" onclick="saveDescAndExit()">'''
new_desc_section = desc_csv_buttons + ''' <div class="action-buttons">
<button type="button" class="btn btn-primary" onclick="saveDescAndExit()">'''
if old_desc_section in content and desc_csv_buttons not in content:
content = content.replace(old_desc_section, new_desc_section)
print("[OK] Added CSV buttons to Description module")
# 5. 加入 CSV 處理函數
csv_functions = '''
// ==================== CSV 匯入匯出函數 ====================
// 崗位資料 CSV 匯出
function exportPositionsCSV() {
// 收集所有崗位資料(這裡簡化為當前表單資料)
const data = [{
positionCode: getFieldValue('positionCode'),
positionName: getFieldValue('positionName'),
positionCategory: getFieldValue('positionCategory'),
positionNature: getFieldValue('positionNature'),
headcount: getFieldValue('headcount'),
positionLevel: getFieldValue('positionLevel'),
effectiveDate: getFieldValue('effectiveDate'),
positionDesc: getFieldValue('positionDesc'),
positionRemark: getFieldValue('positionRemark'),
minEducation: getFieldValue('minEducation'),
salaryRange: getFieldValue('salaryRange'),
workExperience: getFieldValue('workExperience'),
minAge: getFieldValue('minAge'),
maxAge: getFieldValue('maxAge')
}];
const headers = ['positionCode', 'positionName', 'positionCategory', 'positionNature',
'headcount', 'positionLevel', 'effectiveDate', 'positionDesc', 'positionRemark',
'minEducation', 'salaryRange', 'workExperience', 'minAge', 'maxAge'];
CSVUtils.exportToCSV(data, 'positions.csv', headers);
showToast('崗位資料已匯出!');
}
// 崗位資料 CSV 匯入觸發
function importPositionsCSV() {
document.getElementById('positionCSVInput').click();
}
// 處理崗位 CSV 匯入
function handlePositionCSVImport(event) {
const file = event.target.files[0];
if (!file) return;
CSVUtils.importFromCSV(file, (data) => {
if (data && data.length > 0) {
const firstRow = data[0];
// 填充表單
Object.keys(firstRow).forEach(key => {
const element = document.getElementById(key);
if (element) {
element.value = firstRow[key];
}
});
showToast(`已匯入 ${data.length} 筆崗位資料(顯示第一筆)`);
}
});
// 重置 input
event.target.value = '';
}
// 職務資料 CSV 匯出
function exportJobsCSV() {
const data = [{
jobCategoryCode: getFieldValue('jobCategoryCode'),
jobCategoryName: getFieldValue('jobCategoryName'),
jobCode: getFieldValue('jobCode'),
jobName: getFieldValue('jobName'),
jobNameEn: getFieldValue('jobNameEn'),
jobEffectiveDate: getFieldValue('jobEffectiveDate'),
jobHeadcount: getFieldValue('jobHeadcount'),
jobSortOrder: getFieldValue('jobSortOrder'),
jobRemark: getFieldValue('jobRemark'),
jobLevel: getFieldValue('jobLevel'),
hasAttendanceBonus: document.getElementById('hasAttendanceBonus')?.checked,
hasHousingAllowance: document.getElementById('hasHousingAllowance')?.checked
}];
const headers = ['jobCategoryCode', 'jobCategoryName', 'jobCode', 'jobName', 'jobNameEn',
'jobEffectiveDate', 'jobHeadcount', 'jobSortOrder', 'jobRemark', 'jobLevel',
'hasAttendanceBonus', 'hasHousingAllowance'];
CSVUtils.exportToCSV(data, 'jobs.csv', headers);
showToast('職務資料已匯出!');
}
// 職務資料 CSV 匯入觸發
function importJobsCSV() {
document.getElementById('jobCSVInput').click();
}
// 處理職務 CSV 匯入
function handleJobCSVImport(event) {
const file = event.target.files[0];
if (!file) return;
CSVUtils.importFromCSV(file, (data) => {
if (data && data.length > 0) {
const firstRow = data[0];
Object.keys(firstRow).forEach(key => {
const element = document.getElementById(key);
if (element) {
if (element.type === 'checkbox') {
element.checked = firstRow[key] === 'true';
} else {
element.value = firstRow[key];
}
}
});
showToast(`已匯入 ${data.length} 筆職務資料(顯示第一筆)`);
}
});
event.target.value = '';
}
// 崗位描述 CSV 匯出
function exportDescriptionsCSV() {
const data = [{
descPositionCode: getFieldValue('descPositionCode'),
descPositionName: getFieldValue('descPositionName'),
descEffectiveDate: getFieldValue('descEffectiveDate'),
jobDuties: getFieldValue('jobDuties'),
requiredSkills: getFieldValue('requiredSkills'),
workEnvironment: getFieldValue('workEnvironment'),
careerPath: getFieldValue('careerPath'),
descRemark: getFieldValue('descRemark')
}];
const headers = ['descPositionCode', 'descPositionName', 'descEffectiveDate', 'jobDuties',
'requiredSkills', 'workEnvironment', 'careerPath', 'descRemark'];
CSVUtils.exportToCSV(data, 'job_descriptions.csv', headers);
showToast('崗位描述已匯出!');
}
// 崗位描述 CSV 匯入觸發
function importDescriptionsCSV() {
document.getElementById('descCSVInput').click();
}
// 處理崗位描述 CSV 匯入
function handleDescCSVImport(event) {
const file = event.target.files[0];
if (!file) return;
CSVUtils.importFromCSV(file, (data) => {
if (data && data.length > 0) {
const firstRow = data[0];
Object.keys(firstRow).forEach(key => {
const element = document.getElementById(key);
if (element) {
element.value = firstRow[key];
}
});
showToast(`已匯入 ${data.length} 筆崗位描述資料(顯示第一筆)`);
}
});
event.target.value = '';
}
'''
# 在 </script> 前加入函數
if 'function exportPositionsCSV()' not in content:
content = content.replace(' </script>\n</body>', csv_functions + '\n </script>\n</body>')
print("[OK] Added CSV handler functions")
# 寫回
with open('index.html', 'w', encoding='utf-8') as f:
f.write(content)
print("\n" + "="*60)
print("[OK] CSV Integration Complete!")
print("="*60)
print("\nCompleted tasks:")
print("1. Added csv_utils.js reference in <head>")
print("2. Added CSV buttons to Position module")
print("3. Added CSV buttons to Job module")
print("4. Added CSV buttons to Description module")
print("5. Added all CSV handler functions")
print("\nPlease reload the page (Ctrl+F5) to test CSV features!")

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

View File

@@ -0,0 +1,206 @@
# -*- 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_dept_field = ''' <div class="form-group">
<label>所屬部門</label>
<input type="text" id="jd_department" name="department" placeholder="請輸入所屬部門">
</div>
<div class="form-group">
<label>崗位生效日期</label>'''
new_dept_field = ''' <div class="form-group">
<label>所屬部門</label>
<input type="text" id="jd_department" name="department" placeholder="請輸入所屬部門">
</div>
<div class="form-group">
<label>部門職責</label>
<div class="input-wrapper">
<select id="jd_deptFunction" name="deptFunction" onchange="loadDeptFunctionInfo()">
<option value="">-- 請選擇部門職責 --</option>
</select>
<button type="button" class="btn-icon" onclick="refreshDeptFunctionList()" title="重新載入">
<svg viewBox="0 0 24 24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
</button>
</div>
</div>
<div class="form-group">
<label>崗位生效日期</label>'''
if old_dept_field in content and 'jd_deptFunction' not in content:
content = content.replace(old_dept_field, new_dept_field)
print("[OK] Added Department Function dropdown to Job Description form")
else:
print("[INFO] Department Function field already exists or pattern not found")
# ==================== 2. 添加部門職責資訊顯示區塊 ====================
# 在崗位基本信息 section 後面添加部門職責資訊區塊
old_section_end = ''' <div class="form-group">
<label>直接下級(職位及人數)</label>
<input type="text" id="jd_directReports" name="directReports" placeholder="如:工程師 x 5人">
</div>'''
# 找到直接下級後面的結構
new_section_end = ''' <div class="form-group">
<label>直接下級(職位及人數)</label>
<input type="text" id="jd_directReports" name="directReports" placeholder="如:工程師 x 5人">
</div>
</div>
</div>
</div>
<!-- 部門職責資訊 Section (關聯顯示) -->
<div class="section-box" id="deptFunctionInfoSection" style="margin-top: 24px; display: none;">
<div class="section-header" style="background: linear-gradient(135deg, #8e44ad 0%, #9b59b6 100%);">部門職責資訊 (自動帶入)</div>
<div class="section-body">
<div class="form-grid">
<div class="form-group">
<label>部門職責編號</label>
<input type="text" id="jd_deptFunctionCode" readonly style="background: #f8f9fa;">
</div>
<div class="form-group">
<label>事業體</label>
<input type="text" id="jd_deptFunctionBU" readonly style="background: #f8f9fa;">
</div>
</div>
<div class="form-group full-width">
<label>部門使命</label>
<textarea id="jd_deptMission" readonly rows="2" style="background: #f8f9fa;"></textarea>
</div>
<div class="form-group full-width">
<label>部門核心職責</label>
<textarea id="jd_deptCoreFunctions" readonly rows="4" style="background: #f8f9fa;"></textarea>
</div>
<div class="form-group full-width">
<label>部門 KPIs</label>
<textarea id="jd_deptKPIs" readonly rows="3" style="background: #f8f9fa;"></textarea>
</div>
</div>
</div>
<!-- 崗位職責 Section '''
# 找到崗位職責 Section 的開始位置
jobdesc_section_pattern = ''' </div>
</div>
</div>
<!-- 崗位職責 Section -->'''
if jobdesc_section_pattern in content and 'deptFunctionInfoSection' not in content:
content = content.replace(old_section_end + '''
</div>
</div>
</div>
<!-- 崗位職責 Section -->''', new_section_end + '''-->''')
print("[OK] Added Department Function info section to Job Description")
else:
print("[INFO] Dept Function info section already exists or pattern not found - trying alternative approach")
# ==================== 3. 添加相關 JavaScript 函數 ====================
dept_relation_js = '''
// ==================== 部門職責關聯功能 ====================
// 重新載入部門職責下拉選單
function refreshDeptFunctionList() {
const select = document.getElementById('jd_deptFunction');
if (!select) return;
// 清空現有選項
select.innerHTML = '<option value="">-- 請選擇部門職責 --</option>';
// 從 deptFunctionData 載入選項
if (typeof deptFunctionData !== 'undefined' && deptFunctionData.length > 0) {
deptFunctionData.forEach(df => {
const option = document.createElement('option');
option.value = df.deptFunctionCode;
option.textContent = `${df.deptFunctionCode} - ${df.deptFunctionName} (${df.deptFunctionDept})`;
select.appendChild(option);
});
showToast('已載入 ' + deptFunctionData.length + ' 筆部門職責資料');
} else {
showToast('尚無部門職責資料,請先建立部門職責');
}
}
// 載入選中的部門職責資訊
function loadDeptFunctionInfo() {
const select = document.getElementById('jd_deptFunction');
const infoSection = document.getElementById('deptFunctionInfoSection');
if (!select || !infoSection) return;
const selectedCode = select.value;
if (!selectedCode) {
infoSection.style.display = 'none';
return;
}
// 從 deptFunctionData 找到對應的資料
if (typeof deptFunctionData !== 'undefined') {
const deptFunc = deptFunctionData.find(d => d.deptFunctionCode === selectedCode);
if (deptFunc) {
// 填入部門職責資訊
document.getElementById('jd_deptFunctionCode').value = deptFunc.deptFunctionCode || '';
document.getElementById('jd_deptFunctionBU').value = deptFunc.deptFunctionBU || '';
document.getElementById('jd_deptMission').value = deptFunc.deptMission || '';
document.getElementById('jd_deptCoreFunctions').value = deptFunc.deptCoreFunctions || '';
document.getElementById('jd_deptKPIs').value = deptFunc.deptKPIs || '';
// 自動填入所屬部門
const deptInput = document.getElementById('jd_department');
if (deptInput && !deptInput.value) {
deptInput.value = deptFunc.deptFunctionDept;
}
// 顯示部門職責資訊區塊
infoSection.style.display = 'block';
showToast('已載入部門職責: ' + deptFunc.deptFunctionName);
}
}
}
// 在頁面載入時初始化部門職責下拉選單
document.addEventListener('DOMContentLoaded', function() {
// 延遲載入,確保 deptFunctionData 已初始化
setTimeout(refreshDeptFunctionList, 500);
});
'''
# 在部門職責模組功能之後插入
dept_module_js_end = ' // ==================== 管理者頁面功能 ===================='
if dept_module_js_end in content and 'refreshDeptFunctionList' not in content:
content = content.replace(dept_module_js_end, dept_relation_js + dept_module_js_end)
print("[OK] Added Department Function relation JavaScript functions")
else:
print("[INFO] Dept Function relation JS already exists or pattern not found")
# 寫回檔案
with open('index.html', 'w', encoding='utf-8') as f:
f.write(content)
print("\n[DONE] Department Function relation added!")
print("- Added Department Function dropdown to Job Description form")
print("- Added Department Function info display section")
print("- Added JavaScript functions for loading dept function data")

84
scripts/add_org_fields.py Normal file
View File

@@ -0,0 +1,84 @@
"""
新增事業體和組織單位欄位到崗位資料表單
"""
import sys
import codecs
# Windows 編碼修正
if sys.platform == 'win32':
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict')
sys.stderr = codecs.getwriter('utf-8')(sys.stderr.buffer, 'strict')
with open('index.html', 'r', encoding='utf-8') as f:
content = f.read()
# 備份
with open('index.html.backup_org', 'w', encoding='utf-8') as f:
f.write(content)
# 找到基礎資料頁籤中的表單欄位區域,在 positionRemark 欄位前加入新欄位
# 先找到 positionRemark 的 form-group
org_fields_html = ''' <!-- 事業體 -->
<div class="form-group">
<label>事業體 (Business Unit)</label>
<select id="businessUnit" name="businessUnit">
<option value="">請選擇</option>
<option value="SBU">SBU - 銷售事業體</option>
<option value="MBU">MBU - 製造事業體</option>
<option value="HQBU">HQBU - 總部事業體</option>
<option value="ITBU">ITBU - IT事業體</option>
<option value="HRBU">HRBU - HR事業體</option>
<option value="ACCBU">ACCBU - 會計事業體</option>
</select>
</div>
<!-- 處級單位 -->
<div class="form-group">
<label>處級單位 (Division)</label>
<input type="text" id="division" name="division" placeholder="選填">
</div>
<!-- 部級單位 -->
<div class="form-group">
<label>部級單位 (Department)</label>
<input type="text" id="department" name="department" placeholder="選填">
</div>
<!-- 課級單位 -->
<div class="form-group">
<label>課級單位 (Section)</label>
<input type="text" id="section" name="section" placeholder="選填">
</div>
'''
# 在 positionRemark 前插入
old_pattern = ''' <div class="form-group full-width">
<label>崗位備注</label>
<textarea id="positionRemark" name="positionRemark" placeholder="請輸入備注說明..." rows="5"></textarea>
</div>'''
new_pattern = org_fields_html + ''' <div class="form-group full-width">
<label>崗位備注</label>
<textarea id="positionRemark" name="positionRemark" placeholder="請輸入備注說明..." rows="5"></textarea>
</div>'''
if old_pattern in content and org_fields_html not in content:
content = content.replace(old_pattern, new_pattern)
print("[OK] Added organization fields to Position form")
else:
print("[INFO] Organization fields may already exist or pattern not found")
# 寫回
with open('index.html', 'w', encoding='utf-8') as f:
f.write(content)
print("\n" + "="*60)
print("[OK] Organization Fields Added!")
print("="*60)
print("\nAdded fields:")
print("1. Business Unit (SBU/MBU/HQBU/ITBU/HRBU/ACCBU)")
print("2. Division (Division level, optional)")
print("3. Department (Department level, optional)")
print("4. Section (Section level, optional)")
print("\nPlease reload the page (Ctrl+F5) to see the new fields!")

View File

@@ -0,0 +1,499 @@
"""
新增崗位清單頁籤(含排序功能)和管理者頁面
"""
import sys
import codecs
# Windows 編碼修正
if sys.platform == 'win32':
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict')
sys.stderr = codecs.getwriter('utf-8')(sys.stderr.buffer, 'strict')
with open('index.html', 'r', encoding='utf-8') as f:
content = f.read()
# 備份
with open('index.html.backup_list_admin', 'w', encoding='utf-8') as f:
f.write(content)
# 1. 在模組選擇區加入兩個新按鈕
new_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">
<svg viewBox="0 0 24 24"><path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/></svg>
崗位清單
</button>
<button class="module-btn" data-module="admin">
<svg viewBox="0 0 24 24"><path d="M19.14,12.94c0.04-0.31,0.06-0.63,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>
管理者頁面
</button>
</div>'''
old_module_end = ''' <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>
</div>'''
if old_module_end in content and 'data-module="positionlist"' not in content:
content = content.replace(old_module_end, new_module_buttons)
print("[OK] Added Position List and Admin module buttons")
# 2. 找到插入新模組內容的位置(在 </body> 前,<script> 前)
# 先找到最後一個模組結束的位置
# 崗位清單模組 HTML
position_list_module = '''
<!-- ==================== 崗位清單模組 ==================== -->
<div class="module-content" id="module-positionlist">
<header class="app-header" style="background: linear-gradient(135deg, #8e44ad 0%, #9b59b6 100%);">
<div class="icon">
<svg viewBox="0 0 24 24" style="fill: white;"><path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/></svg>
</div>
<div>
<h1 style="color: white;">崗位清單</h1>
<div class="subtitle" style="color: rgba(255,255,255,0.8);">Position List with Sorting</div>
</div>
</header>
<div class="form-card">
<div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
<div>
<button type="button" class="btn btn-primary" onclick="loadPositionList()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
載入清單
</button>
<button type="button" class="btn btn-secondary" onclick="exportPositionListCSV()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>
匯出 CSV
</button>
</div>
<div style="color: var(--text-secondary);">
點擊欄位標題進行排序
</div>
</div>
<div style="overflow-x: auto;">
<table id="positionListTable" style="width: 100%; border-collapse: collapse; background: white;">
<thead>
<tr style="background: var(--primary); color: white;">
<th class="sortable" data-sort="positionCode" onclick="sortPositionList('positionCode')" style="padding: 12px; cursor: pointer; text-align: left;">
崗位編號 <span class="sort-icon"></span>
</th>
<th class="sortable" data-sort="positionName" onclick="sortPositionList('positionName')" style="padding: 12px; cursor: pointer; text-align: left;">
崗位名稱 <span class="sort-icon"></span>
</th>
<th class="sortable" data-sort="businessUnit" onclick="sortPositionList('businessUnit')" style="padding: 12px; cursor: pointer; text-align: left;">
事業體 <span class="sort-icon"></span>
</th>
<th class="sortable" data-sort="department" onclick="sortPositionList('department')" style="padding: 12px; cursor: pointer; text-align: left;">
部門 <span class="sort-icon"></span>
</th>
<th class="sortable" data-sort="positionCategory" onclick="sortPositionList('positionCategory')" style="padding: 12px; cursor: pointer; text-align: left;">
崗位類別 <span class="sort-icon"></span>
</th>
<th class="sortable" data-sort="headcount" onclick="sortPositionList('headcount')" style="padding: 12px; cursor: pointer; text-align: left;">
編制人數 <span class="sort-icon"></span>
</th>
<th class="sortable" data-sort="effectiveDate" onclick="sortPositionList('effectiveDate')" style="padding: 12px; cursor: pointer; text-align: left;">
生效日期 <span class="sort-icon"></span>
</th>
<th style="padding: 12px; text-align: center;">操作</th>
</tr>
</thead>
<tbody id="positionListBody">
<tr>
<td colspan="8" style="padding: 40px; text-align: center; color: var(--text-secondary);">
點擊「載入清單」按鈕以顯示崗位資料
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ==================== 管理者頁面模組 ==================== -->
<div class="module-content" id="module-admin">
<header class="app-header" style="background: linear-gradient(135deg, #c0392b 0%, #e74c3c 100%);">
<div class="icon">
<svg viewBox="0 0 24 24" style="fill: white;"><path d="M19.14,12.94c0.04-0.31,0.06-0.63,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>
</div>
<div>
<h1 style="color: white;">管理者頁面</h1>
<div class="subtitle" style="color: rgba(255,255,255,0.8);">User Administration</div>
</div>
</header>
<div class="form-card">
<div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
<h2 style="color: var(--primary); margin: 0;">使用者清單</h2>
<div style="display: flex; gap: 10px;">
<button type="button" class="btn btn-primary" onclick="showAddUserModal()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
新增使用者
</button>
<button type="button" class="btn btn-secondary" onclick="exportUsersCSV()">
<svg viewBox="0 0 24 24" style="width: 18px; height: 18px; fill: currentColor;"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>
匯出 CSV
</button>
</div>
</div>
<div style="overflow-x: auto;">
<table id="userListTable" style="width: 100%; border-collapse: collapse; background: white;">
<thead>
<tr style="background: #c0392b; color: white;">
<th style="padding: 12px; text-align: left;">工號</th>
<th style="padding: 12px; text-align: left;">使用者姓名</th>
<th style="padding: 12px; text-align: left;">Email 信箱</th>
<th style="padding: 12px; text-align: left;">權限等級</th>
<th style="padding: 12px; text-align: left;">建立日期</th>
<th style="padding: 12px; text-align: center;">操作</th>
</tr>
</thead>
<tbody id="userListBody">
<tr>
<td style="padding: 12px; border-bottom: 1px solid #eee;">A001</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">系統管理員</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">admin@company.com</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">
<span style="background: #e74c3c; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">最高權限管理者</span>
</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">2024-01-01</td>
<td style="padding: 12px; border-bottom: 1px solid #eee; text-align: center;">
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;" onclick="editUser('A001')">編輯</button>
</td>
</tr>
<tr>
<td style="padding: 12px; border-bottom: 1px solid #eee;">A002</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">人資主管</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">hr_manager@company.com</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">
<span style="background: #f39c12; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">管理者</span>
</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">2024-01-15</td>
<td style="padding: 12px; border-bottom: 1px solid #eee; text-align: center;">
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;" onclick="editUser('A002')">編輯</button>
<button class="btn btn-cancel" style="padding: 4px 8px; font-size: 12px;" onclick="deleteUser('A002')">刪除</button>
</td>
</tr>
<tr>
<td style="padding: 12px; border-bottom: 1px solid #eee;">A003</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">一般員工</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">employee@company.com</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">
<span style="background: #27ae60; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">一般使用者</span>
</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">2024-02-01</td>
<td style="padding: 12px; border-bottom: 1px solid #eee; text-align: center;">
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;" onclick="editUser('A003')">編輯</button>
<button class="btn btn-cancel" style="padding: 4px 8px; font-size: 12px;" onclick="deleteUser('A003')">刪除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 新增/編輯使用者彈窗 -->
<div id="userModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; justify-content: center; align-items: center;">
<div style="background: white; border-radius: 8px; padding: 30px; max-width: 500px; width: 90%;">
<h3 id="userModalTitle" style="margin: 0 0 20px 0; color: var(--primary);">新增使用者</h3>
<form id="userForm">
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500;">工號 <span style="color: red;">*</span></label>
<input type="text" id="userEmployeeId" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500;">使用者姓名 <span style="color: red;">*</span></label>
<input type="text" id="userName" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500;">Email 信箱 <span style="color: red;">*</span></label>
<input type="email" id="userEmail" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 5px; font-weight: 500;">權限等級 <span style="color: red;">*</span></label>
<select id="userRole" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
<option value="">請選擇</option>
<option value="user">一般使用者</option>
<option value="admin">管理者</option>
<option value="superadmin">最高權限管理者</option>
</select>
</div>
<div style="display: flex; justify-content: flex-end; gap: 10px;">
<button type="button" class="btn btn-cancel" onclick="closeUserModal()">取消</button>
<button type="submit" class="btn btn-primary" onclick="saveUser(event)">儲存</button>
</div>
</form>
</div>
</div>
'''
# 找到 </body> 前插入新模組
if 'module-positionlist' not in content:
# 找到 <script> 標籤的位置
script_start = content.find(' <script>')
if script_start > 0:
content = content[:script_start] + position_list_module + '\n' + content[script_start:]
print("[OK] Added Position List and Admin module content")
# 3. 加入新功能的 JavaScript 函數
new_js_functions = '''
// ==================== 崗位清單功能 ====================
let positionListData = [];
let currentSortColumn = '';
let currentSortDirection = 'asc';
// 載入崗位清單(示範資料)
function loadPositionList() {
// 示範資料
positionListData = [
{ positionCode: 'POS001', positionName: '軟體工程師', businessUnit: 'ITBU', department: '研發部', positionCategory: '技術職', headcount: 5, effectiveDate: '2024-01-01' },
{ positionCode: 'POS002', positionName: '專案經理', businessUnit: 'ITBU', department: '專案管理部', positionCategory: '管理職', headcount: 2, effectiveDate: '2024-01-01' },
{ positionCode: 'POS003', positionName: '人資專員', businessUnit: 'HRBU', department: '人力資源部', positionCategory: '行政職', headcount: 3, effectiveDate: '2024-02-01' },
{ positionCode: 'POS004', positionName: '財務分析師', businessUnit: 'ACCBU', department: '財務部', positionCategory: '專業職', headcount: 2, effectiveDate: '2024-01-15' },
{ positionCode: 'POS005', positionName: '業務代表', businessUnit: 'SBU', department: '業務部', positionCategory: '業務職', headcount: 10, effectiveDate: '2024-03-01' },
{ positionCode: 'POS006', positionName: '生產線主管', businessUnit: 'MBU', department: '生產部', positionCategory: '管理職', headcount: 4, effectiveDate: '2024-01-01' },
];
renderPositionList();
showToast('已載入 ' + positionListData.length + ' 筆崗位資料');
}
// 渲染崗位清單
function renderPositionList() {
const tbody = document.getElementById('positionListBody');
if (positionListData.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" style="padding: 40px; text-align: center; color: var(--text-secondary);">沒有資料</td></tr>';
return;
}
tbody.innerHTML = positionListData.map(item => `
<tr style="border-bottom: 1px solid #eee;">
<td style="padding: 12px;">${item.positionCode}</td>
<td style="padding: 12px;">${item.positionName}</td>
<td style="padding: 12px;">${item.businessUnit}</td>
<td style="padding: 12px;">${item.department}</td>
<td style="padding: 12px;">${item.positionCategory}</td>
<td style="padding: 12px;">${item.headcount}</td>
<td style="padding: 12px;">${item.effectiveDate}</td>
<td style="padding: 12px; text-align: center;">
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;" onclick="viewPosition('${item.positionCode}')">檢視</button>
</td>
</tr>
`).join('');
}
// 排序崗位清單
function sortPositionList(column) {
if (currentSortColumn === column) {
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
} else {
currentSortColumn = column;
currentSortDirection = 'asc';
}
positionListData.sort((a, b) => {
let valA = a[column];
let valB = b[column];
if (typeof valA === 'string') {
valA = valA.toLowerCase();
valB = valB.toLowerCase();
}
if (valA < valB) return currentSortDirection === 'asc' ? -1 : 1;
if (valA > valB) return currentSortDirection === 'asc' ? 1 : -1;
return 0;
});
// 更新排序圖示
document.querySelectorAll('.sort-icon').forEach(icon => icon.textContent = '');
const currentHeader = document.querySelector(`th[data-sort="${column}"] .sort-icon`);
if (currentHeader) {
currentHeader.textContent = currentSortDirection === 'asc' ? ' ^' : ' v';
}
renderPositionList();
}
// 檢視崗位
function viewPosition(code) {
const position = positionListData.find(p => p.positionCode === code);
if (position) {
showToast('檢視崗位: ' + position.positionName);
}
}
// 匯出崗位清單 CSV
function exportPositionListCSV() {
if (positionListData.length === 0) {
showToast('請先載入清單資料');
return;
}
const headers = ['positionCode', 'positionName', 'businessUnit', 'department', 'positionCategory', 'headcount', 'effectiveDate'];
CSVUtils.exportToCSV(positionListData, 'position_list.csv', headers);
showToast('崗位清單已匯出!');
}
// ==================== 管理者頁面功能 ====================
let usersData = [
{ employeeId: 'A001', name: '系統管理員', email: 'admin@company.com', role: 'superadmin', createdAt: '2024-01-01' },
{ employeeId: 'A002', name: '人資主管', email: 'hr_manager@company.com', role: 'admin', createdAt: '2024-01-15' },
{ employeeId: 'A003', name: '一般員工', email: 'employee@company.com', role: 'user', createdAt: '2024-02-01' }
];
let editingUserId = null;
// 顯示新增使用者彈窗
function showAddUserModal() {
editingUserId = null;
document.getElementById('userModalTitle').textContent = '新增使用者';
document.getElementById('userEmployeeId').value = '';
document.getElementById('userName').value = '';
document.getElementById('userEmail').value = '';
document.getElementById('userRole').value = '';
document.getElementById('userEmployeeId').disabled = false;
document.getElementById('userModal').style.display = 'flex';
}
// 編輯使用者
function editUser(employeeId) {
const user = usersData.find(u => u.employeeId === employeeId);
if (!user) return;
editingUserId = employeeId;
document.getElementById('userModalTitle').textContent = '編輯使用者';
document.getElementById('userEmployeeId').value = user.employeeId;
document.getElementById('userEmployeeId').disabled = true;
document.getElementById('userName').value = user.name;
document.getElementById('userEmail').value = user.email;
document.getElementById('userRole').value = user.role;
document.getElementById('userModal').style.display = 'flex';
}
// 關閉使用者彈窗
function closeUserModal() {
document.getElementById('userModal').style.display = 'none';
editingUserId = null;
}
// 儲存使用者
function saveUser(event) {
event.preventDefault();
const employeeId = document.getElementById('userEmployeeId').value;
const name = document.getElementById('userName').value;
const email = document.getElementById('userEmail').value;
const role = document.getElementById('userRole').value;
if (!employeeId || !name || !email || !role) {
showToast('請填寫所有必填欄位');
return;
}
if (editingUserId) {
// 編輯模式
const index = usersData.findIndex(u => u.employeeId === editingUserId);
if (index > -1) {
usersData[index] = { ...usersData[index], name, email, role };
showToast('使用者已更新');
}
} else {
// 新增模式
if (usersData.some(u => u.employeeId === employeeId)) {
showToast('工號已存在');
return;
}
usersData.push({
employeeId,
name,
email,
role,
createdAt: new Date().toISOString().split('T')[0]
});
showToast('使用者已新增');
}
closeUserModal();
renderUserList();
}
// 刪除使用者
function deleteUser(employeeId) {
if (confirm('確定要刪除此使用者嗎?')) {
usersData = usersData.filter(u => u.employeeId !== employeeId);
renderUserList();
showToast('使用者已刪除');
}
}
// 渲染使用者清單
function renderUserList() {
const tbody = document.getElementById('userListBody');
const roleLabels = {
'superadmin': { text: '最高權限管理者', color: '#e74c3c' },
'admin': { text: '管理者', color: '#f39c12' },
'user': { text: '一般使用者', color: '#27ae60' }
};
tbody.innerHTML = usersData.map(user => {
const roleInfo = roleLabels[user.role] || { text: user.role, color: '#999' };
const isSuperAdmin = user.role === 'superadmin';
return `
<tr>
<td style="padding: 12px; border-bottom: 1px solid #eee;">${user.employeeId}</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">${user.name}</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">${user.email}</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">
<span style="background: ${roleInfo.color}; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">${roleInfo.text}</span>
</td>
<td style="padding: 12px; border-bottom: 1px solid #eee;">${user.createdAt}</td>
<td style="padding: 12px; border-bottom: 1px solid #eee; text-align: center;">
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 12px;" onclick="editUser('${user.employeeId}')">編輯</button>
${!isSuperAdmin ? `<button class="btn btn-cancel" style="padding: 4px 8px; font-size: 12px;" onclick="deleteUser('${user.employeeId}')">刪除</button>` : ''}
</td>
</tr>
`;
}).join('');
}
// 匯出使用者 CSV
function exportUsersCSV() {
const headers = ['employeeId', 'name', 'email', 'role', 'createdAt'];
CSVUtils.exportToCSV(usersData, 'users.csv', headers);
showToast('使用者清單已匯出!');
}
'''
# 在現有的 JavaScript 函數區塊末尾加入新函數
if 'function loadPositionList()' not in content:
# 找到 </script> 的位置
script_end = content.find(' </script>')
if script_end > 0:
content = content[:script_end] + new_js_functions + '\n' + content[script_end:]
print("[OK] Added Position List and Admin JavaScript functions")
# 寫回
with open('index.html', 'w', encoding='utf-8') as f:
f.write(content)
print("\n" + "="*60)
print("[OK] Position List and Admin Page Added!")
print("="*60)
print("\nNew features:")
print("1. Position List tab with sortable columns")
print(" - Click column headers to sort")
print(" - Export to CSV")
print("2. Admin page with user management")
print(" - Add/Edit/Delete users")
print(" - Three permission levels:")
print(" - Regular User")
print(" - Admin")
print(" - Super Admin")
print(" - Export users to CSV")
print("\nPlease reload the page (Ctrl+F5) to see the new features!")

View File

@@ -0,0 +1,258 @@
"""
隨機建立 10 筆崗位資料到系統
從 excel_table copy.md 中隨機選取資料並透過 API 建立
"""
import requests
import random
from datetime import datetime
import sys
import io
# 設定 UTF-8 輸出
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
# 從 excel_table copy.md 中讀取的組織及崗位資料
org_positions = [
{"business": "岡山製造事業體", "division": "生產處", "department": "生產部", "position": "課長"},
{"business": "岡山製造事業體", "division": "生產處", "department": "生產部", "position": "組長"},
{"business": "岡山製造事業體", "division": "生產處", "department": "生產部", "position": "班長"},
{"business": "岡山製造事業體", "division": "生產處", "department": "生產部", "position": "副班長"},
{"business": "岡山製造事業體", "division": "生產處", "department": "生產部", "position": "作業員"},
{"business": "岡山製造事業體", "division": "生產處", "department": "生產企劃部", "position": "經副理"},
{"business": "岡山製造事業體", "division": "生產處", "department": "生產企劃部", "position": "課長"},
{"business": "岡山製造事業體", "division": "生產處", "department": "生產企劃部", "position": "專員"},
{"business": "岡山製造事業體", "division": "生產處", "department": "生產企劃部", "position": "工程師"},
{"business": "岡山製造事業體", "division": "岡山製造事業體", "department": "岡山品質管制部", "position": "經副理"},
{"business": "岡山製造事業體", "division": "岡山製造事業體", "department": "岡山品質管制部", "position": "課長"},
{"business": "岡山製造事業體", "division": "岡山製造事業體", "department": "岡山品質管制部", "position": "工程師"},
{"business": "岡山製造事業體", "division": "岡山製造事業體", "department": "岡山品質管制部", "position": "組長"},
{"business": "岡山製造事業體", "division": "岡山製造事業體", "department": "岡山品質管制部", "position": "班長"},
{"business": "岡山製造事業體", "division": "岡山製造事業體", "department": "岡山品質管制部", "position": "副班長"},
{"business": "岡山製造事業體", "division": "岡山製造事業體", "department": "岡山品質管制部", "position": "作業員"},
{"business": "岡山製造事業體", "division": "封裝工程處", "department": "", "position": "處長"},
{"business": "岡山製造事業體", "division": "封裝工程處", "department": "", "position": "專員"},
{"business": "岡山製造事業體", "division": "封裝工程處", "department": "", "position": "工程師"},
{"business": "岡山製造事業體", "division": "封裝工程處", "department": "製程工程一部", "position": "經副理"},
{"business": "岡山製造事業體", "division": "封裝工程處", "department": "製程工程二部", "position": "經副理"},
{"business": "岡山製造事業體", "division": "封裝工程處", "department": "製程工程二部", "position": "課長"},
{"business": "岡山製造事業體", "division": "封裝工程處", "department": "製程工程二部", "position": "工程師"},
{"business": "岡山製造事業體", "division": "封裝工程處", "department": "設備一部", "position": "經副理"},
{"business": "岡山製造事業體", "division": "封裝工程處", "department": "設備二部", "position": "經副理"},
{"business": "岡山製造事業體", "division": "封裝工程處", "department": "設備二部", "position": "課長"},
{"business": "岡山製造事業體", "division": "封裝工程處", "department": "設備二部", "position": "工程師"},
{"business": "岡山製造事業體", "division": "副總辦公室", "department": "工業工程部", "position": "經副理"},
{"business": "岡山製造事業體", "division": "副總辦公室", "department": "工業工程部", "position": "工程師"},
{"business": "岡山製造事業體", "division": "副總辦公室", "department": "工業工程部", "position": "課長"},
{"business": "岡山製造事業體", "division": "副總辦公室", "department": "工業工程部", "position": "副理"},
{"business": "岡山製造事業體", "division": "測試工程與研發處", "department": "", "position": "處長"},
{"business": "岡山製造事業體", "division": "測試工程與研發處", "department": "", "position": "專員"},
{"business": "岡山製造事業體", "division": "測試工程與研發處", "department": "測試工程部", "position": "經副理"},
{"business": "岡山製造事業體", "division": "測試工程與研發處", "department": "測試工程部", "position": "課長"},
{"business": "岡山製造事業體", "division": "測試工程與研發處", "department": "測試工程部", "position": "工程師"},
{"business": "岡山製造事業體", "division": "測試工程與研發處", "department": "新產品導入部", "position": "經副理"},
{"business": "岡山製造事業體", "division": "測試工程與研發處", "department": "新產品導入部", "position": "專員"},
{"business": "岡山製造事業體", "division": "測試工程與研發處", "department": "新產品導入部", "position": "工程師"},
{"business": "岡山製造事業體", "division": "測試工程與研發處", "department": "研發部", "position": "經副理"},
{"business": "產品事業體", "division": "先進產品事業處", "department": "產品管理部(APD)", "position": "經副理"},
{"business": "產品事業體", "division": "先進產品事業處", "department": "產品管理部(APD)", "position": "工程師"},
{"business": "產品事業體", "division": "成熟產品事業處", "department": "產品管理部(MPD)", "position": "經副理"},
{"business": "產品事業體", "division": "成熟產品事業處", "department": "產品管理部(MPD)", "position": "專案經副理"},
{"business": "產品事業體", "division": "成熟產品事業處", "department": "產品管理部(MPD)", "position": "工程師"},
{"business": "晶圓三廠", "division": "晶圓三廠", "department": "品質部", "position": "經副理"},
{"business": "晶圓三廠", "division": "晶圓三廠", "department": "品質部", "position": "工程師"},
{"business": "晶圓三廠", "division": "晶圓三廠", "department": "製造部", "position": "經副理"},
{"business": "晶圓三廠", "division": "晶圓三廠", "department": "製造部", "position": "課長"},
{"business": "晶圓三廠", "division": "晶圓三廠", "department": "製造部", "position": "班長"},
{"business": "晶圓三廠", "division": "製程工程處", "department": "工程一部", "position": "經副理"},
{"business": "晶圓三廠", "division": "製程工程處", "department": "工程一部", "position": "工程師"},
{"business": "晶圓三廠", "division": "製程工程處", "department": "工程二部", "position": "經副理"},
{"business": "晶圓三廠", "division": "製程工程處", "department": "工程二部", "position": "工程師"},
{"business": "集團人資行政事業體", "division": "集團人資行政事業體", "department": "招募任用部", "position": "經副理"},
{"business": "集團人資行政事業體", "division": "集團人資行政事業體", "department": "招募任用部", "position": "專員"},
{"business": "集團人資行政事業體", "division": "集團人資行政事業體", "department": "訓練發展部", "position": "經副理"},
{"business": "集團人資行政事業體", "division": "集團人資行政事業體", "department": "訓練發展部", "position": "專員"},
{"business": "集團人資行政事業體", "division": "集團人資行政事業體", "department": "薪酬管理部", "position": "經副理"},
{"business": "集團人資行政事業體", "division": "集團人資行政事業體", "department": "薪酬管理部", "position": "專員"},
]
# 崗位類別對應
position_category_map = {
"處長": "02", # 管理職
"經副理": "02",
"副理": "02",
"課長": "02",
"組長": "02",
"班長": "02",
"副班長": "02",
"工程師": "01", # 技術職
"專員": "04", # 行政職
"作業員": "06", # 生產職
}
# 崗位等級對應
position_level_map = {
"處長": "L7",
"經副理": "L5",
"副理": "L5",
"課長": "L4",
"組長": "L3",
"班長": "L3",
"副班長": "L2",
"工程師": "L3",
"專員": "L3",
"作業員": "L1",
}
# 學歷要求對應
education_map = {
"處長": "MA", # 碩士
"經副理": "BA", # 大學
"副理": "BA",
"課長": "BA",
"組長": "JC", # 專科
"班長": "JC",
"副班長": "HS", # 高中職
"工程師": "BA",
"專員": "BA",
"作業員": "HS",
}
# 薪資範圍對應
salary_range_map = {
"處長": "E",
"經副理": "D",
"副理": "D",
"課長": "C",
"組長": "C",
"班長": "B",
"副班長": "B",
"工程師": "C",
"專員": "C",
"作業員": "A",
}
def generate_position_code(business, division, department, position, index):
"""生成崗位編號"""
# 事業體代碼
business_code_map = {
"岡山製造事業體": "KS",
"產品事業體": "PD",
"晶圓三廠": "F3",
"集團人資行政事業體": "HR",
}
# 崗位代碼
position_code_map = {
"處長": "DIR",
"經副理": "MGR",
"副理": "AMG",
"課長": "SUP",
"組長": "LDR",
"班長": "CHF",
"副班長": "ACF",
"工程師": "ENG",
"專員": "SPC",
"作業員": "OPR",
}
biz_code = business_code_map.get(business, "XX")
pos_code = position_code_map.get(position, "XXX")
return f"{biz_code}-{pos_code}-{index:03d}"
def create_position_data(org_data, index):
"""創建崗位資料"""
business = org_data["business"]
division = org_data["division"]
department = org_data["department"]
position = org_data["position"]
position_code = generate_position_code(business, division, department, position, index)
# 組合崗位名稱
dept_str = f"{department}-" if department else ""
position_name = f"{dept_str}{position}"
return {
"basicInfo": {
"positionCode": position_code,
"positionName": position_name,
"positionCategory": position_category_map.get(position, "04"),
"positionCategoryName": "管理職" if position_category_map.get(position, "04") == "02" else "技術職",
"positionNature": "FT",
"positionNatureName": "全職",
"headcount": str(random.randint(1, 5)),
"positionLevel": position_level_map.get(position, "L3"),
"effectiveDate": "2024-01-01",
"positionDesc": f"{business} {division} {position_name}",
"positionRemark": f"組織架構: {business} > {division} > {department if department else '(處級)'}"
},
"recruitInfo": {
"minEducation": education_map.get(position, "BA"),
"requiredGender": "",
"salaryRange": salary_range_map.get(position, "C"),
"workExperience": str(random.randint(0, 5)),
"minAge": "22",
"maxAge": "50",
"jobType": "FT",
"recruitPosition": position,
"jobTitle": position,
"jobDesc": "",
"positionReq": "",
"titleReq": "",
"majorReq": "",
"skillReq": "",
"langReq": "",
"otherReq": "",
"superiorPosition": "",
"recruitRemark": ""
}
}
def main():
"""主程式"""
api_url = "http://localhost:5000/api/positions"
# 隨機選取 10 筆資料
selected = random.sample(org_positions, min(10, len(org_positions)))
print("=" * 60)
print("隨機建立 10 筆崗位資料")
print("=" * 60)
print()
success_count = 0
fail_count = 0
for i, org_data in enumerate(selected, 1):
position_data = create_position_data(org_data, i)
print(f"[{i}/10] 建立崗位: {position_data['basicInfo']['positionCode']} - {position_data['basicInfo']['positionName']}")
try:
response = requests.post(api_url, json=position_data, timeout=5)
if response.status_code == 201:
print(f" [OK] 成功")
success_count += 1
else:
error_msg = response.json().get('error', '未知錯誤')
print(f" [ERROR] 失敗: {error_msg}")
fail_count += 1
except requests.exceptions.ConnectionError:
print(f" [ERROR] 失敗: 無法連接到伺服器 (請確認伺服器是否已啟動)")
fail_count += 1
except Exception as e:
print(f" [ERROR] 失敗: {str(e)}")
fail_count += 1
print()
print("=" * 60)
print(f"建立完成: 成功 {success_count} 筆, 失敗 {fail_count}")
print("=" * 60)
if __name__ == "__main__":
main()

236
scripts/apply_cors_fix.py Normal file
View File

@@ -0,0 +1,236 @@
"""
自動修正 index.html 中的 CORS 錯誤
將直接調用 Claude API 改為通過 Flask 後端調用
"""
import os
import re
from pathlib import Path
def fix_cors_in_index_html():
"""修正 index.html 中的 CORS 問題"""
# 文件路徑
index_path = Path(__file__).parent / 'index.html'
backup_path = Path(__file__).parent / 'index.html.backup'
if not index_path.exists():
print(f"❌ 錯誤: 找不到 {index_path}")
return False
print(f"📂 讀取文件: {index_path}")
# 讀取文件內容
with open(index_path, 'r', encoding='utf-8') as f:
content = f.read()
# 檢查是否已經修正過
if '/api/llm/generate' in content:
print("✓ 文件已經修正過,無需重複修正")
return True
# 備份原文件
print(f"💾 創建備份: {backup_path}")
with open(backup_path, 'w', encoding='utf-8') as f:
f.write(content)
# 原始代碼模式
old_pattern = r'''async function callClaudeAPI\(prompt\) \{
try \{
const response = await fetch\("https://api\.anthropic\.com/v1/messages", \{
method: "POST",
headers: \{
"Content-Type": "application/json",
\},
body: JSON\.stringify\(\{
model: "claude-sonnet-4-20250514",
max_tokens: 2000,
messages: \[
\{ role: "user", content: prompt \}
\]
\}\)
\}\);
if \(!response\.ok\) \{
throw new Error\(`API request failed: \$\{response\.status\}`\);
\}
const data = await response\.json\(\);
let responseText = data\.content\[0\]\.text;
responseText = responseText\.replace\(/```json\\n\?/g, ""\)\.replace\(/```\\n\?/g, ""\)\.trim\(\);
return JSON\.parse\(responseText\);
\} catch \(error\) \{
console\.error\("Error calling Claude API:", error\);
throw error;
\}
\}'''
# 新代碼
new_code = '''async function callClaudeAPI(prompt, api = 'gemini') {
try {
// 調用後端 Flask API避免 CORS 錯誤
const response = await fetch("/api/llm/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
api: api, // 使用指定的 LLM API (gemini, deepseek, openai)
prompt: prompt,
max_tokens: 2000
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `API 請求失敗: ${response.status}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'API 調用失敗');
}
// 解析返回的文字為 JSON
let responseText = data.text;
responseText = responseText.replace(/```json\\n?/g, "").replace(/```\\n?/g, "").trim();
return JSON.parse(responseText);
} catch (error) {
console.error("Error calling LLM API:", error);
// 顯示友好的錯誤訊息
alert(`AI 生成錯誤: ${error.message}\\n\\n請確保\\n1. Flask 後端已啟動 (python app_updated.py)\\n2. 已在 .env 文件中配置 LLM API Key\\n3. 網路連線正常`);
throw error;
}
}'''
# 簡單替換(如果正則表達式匹配失敗)
old_simple = '''async function callClaudeAPI(prompt) {
try {
const response = await fetch("https://api.anthropic.com/v1/messages", {'''
new_simple = '''async function callClaudeAPI(prompt, api = 'gemini') {
try {
// 調用後端 Flask API避免 CORS 錯誤
const response = await fetch("/api/llm/generate", {'''
print("🔧 應用修正...")
# 嘗試正則表達式替換
new_content = re.sub(old_pattern, new_code, content, flags=re.MULTILINE)
# 如果正則表達式沒有匹配,使用簡單替換
if new_content == content:
print("⚠️ 正則表達式未匹配,使用簡單替換...")
# 找到函數的開始和結束
start_marker = 'async function callClaudeAPI(prompt) {'
end_marker = ' }'
start_idx = content.find(start_marker)
if start_idx == -1:
print("❌ 錯誤: 找不到 callClaudeAPI 函數")
return False
# 找到函數結束(找到第一個與縮進匹配的 }
end_idx = start_idx
brace_count = 0
in_function = False
for i in range(start_idx, len(content)):
if content[i] == '{':
brace_count += 1
in_function = True
elif content[i] == '}':
brace_count -= 1
if in_function and brace_count == 0:
end_idx = i + 1
break
if end_idx == start_idx:
print("❌ 錯誤: 無法找到函數結束位置")
return False
# 替換函數
new_content = content[:start_idx] + new_code + content[end_idx:]
# 寫回文件
print(f"💾 保存修正後的文件: {index_path}")
with open(index_path, 'w', encoding='utf-8') as f:
f.write(new_content)
print("\n✅ CORS 修正完成!")
print("\n📋 接下來的步驟:")
print("1. 確保 .env 文件中已配置至少一個 LLM API Key")
print("2. 啟動 Flask 後端: python app_updated.py")
print("3. 在瀏覽器中重新載入頁面 (Ctrl+F5)")
print("4. 測試 AI 自動填充功能")
print(f"\n💡 原文件已備份至: {backup_path}")
return True
def verify_flask_backend():
"""檢查是否有正確的 Flask 後端文件"""
app_updated = Path(__file__).parent / 'app_updated.py'
if not app_updated.exists():
print("\n⚠️ 警告: 找不到 app_updated.py")
print("請確保使用包含 LLM API 端點的 Flask 後端")
return False
print(f"\n✓ 找到更新版 Flask 後端: {app_updated}")
return True
def check_env_file():
"""檢查 .env 文件配置"""
env_path = Path(__file__).parent / '.env'
if not env_path.exists():
print("\n⚠️ 警告: 找不到 .env 文件")
return False
with open(env_path, 'r', encoding='utf-8') as f:
env_content = f.read()
has_gemini = 'GEMINI_API_KEY=' in env_content and 'your_gemini_api_key_here' not in env_content
has_deepseek = 'DEEPSEEK_API_KEY=' in env_content and 'your_deepseek_api_key_here' not in env_content
has_openai = 'OPENAI_API_KEY=' in env_content and 'your_openai_api_key_here' not in env_content
print("\n📋 LLM API Key 配置狀態:")
print(f" {'' if has_gemini else ''} Gemini API Key")
print(f" {'' if has_deepseek else ''} DeepSeek API Key")
print(f" {'' if has_openai else ''} OpenAI API Key")
if not (has_gemini or has_deepseek or has_openai):
print("\n⚠️ 警告: 沒有配置任何 LLM API Key")
print("請編輯 .env 文件,添加至少一個有效的 API Key")
return False
return True
if __name__ == '__main__':
print("=" * 60)
print("HR Position System - CORS 錯誤自動修正工具")
print("=" * 60)
print()
# 修正 CORS 問題
if fix_cors_in_index_html():
# 驗證其他配置
verify_flask_backend()
check_env_file()
print("\n" + "=" * 60)
print("✅ 修正完成!")
print("=" * 60)
else:
print("\n" + "=" * 60)
print("❌ 修正失敗,請查看上述錯誤訊息")
print("=" * 60)
print("\n您也可以手動修正,請參考: CORS_FIX_GUIDE.md")

26
scripts/check_models.py Normal file
View File

@@ -0,0 +1,26 @@
"""
Check available models on Ollama API
"""
import requests
import urllib3
# Disable SSL warnings
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
API_URL = "https://ollama_pjapi.theaken.com"
print("Available models on Ollama API:")
print("-" * 60)
try:
response = requests.get(f"{API_URL}/v1/models", timeout=10, verify=False)
if response.status_code == 200:
data = response.json()
models = data.get('data', [])
for i, model in enumerate(models, 1):
model_id = model.get('id', 'Unknown')
print(f"{i}. {model_id}")
else:
print(f"Error: {response.status_code}")
except Exception as e:
print(f"Error: {str(e)}")

99
scripts/complete_fix.py Normal file
View File

@@ -0,0 +1,99 @@
with open('index.html', 'r', encoding='utf-8') as f:
lines = f.readlines()
# 找到函數的起始行
start_line = None
for i, line in enumerate(lines):
if 'async function callClaudeAPI' in line:
start_line = i
break
if start_line is None:
print("ERROR: Could not find callClaudeAPI function")
exit(1)
# 找到函數的結束行
end_line = None
brace_count = 0
for i in range(start_line, len(lines)):
for char in lines[i]:
if char == '{':
brace_count += 1
elif char == '}':
brace_count -= 1
if brace_count == 0:
end_line = i
break
if end_line:
break
if end_line is None:
print("ERROR: Could not find end of function")
exit(1)
print(f"Found function from line {start_line+1} to {end_line+1}")
# 新函數
new_function = ''' async function callClaudeAPI(prompt, api = 'gemini') {
try {
// 調用後端 Flask API避免 CORS 錯誤
const response = await fetch("/api/llm/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
api: api,
prompt: prompt,
max_tokens: 2000
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `API 請求失敗: ${response.status}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'API 調用失敗');
}
let responseText = data.text;
responseText = responseText.replace(/```json\\n?/g, "").replace(/```\\n?/g, "").trim();
return JSON.parse(responseText);
} catch (error) {
console.error("Error calling LLM API:", error);
alert(`AI 生成錯誤: ${error.message}\\n\\n請確保\\n1. Flask 後端已啟動 (python app_updated.py)\\n2. 已在 .env 文件中配置 LLM API Key\\n3. 網路連線正常`);
throw error;
}
}
'''
# 替換
new_lines = lines[:start_line] + [new_function] + lines[end_line+1:]
# 寫回
with open('index.html', 'w', encoding='utf-8') as f:
f.writelines(new_lines)
print("SUCCESS: Function completely replaced")
print("\nVerifying...")
# 驗證
with open('index.html', 'r', encoding='utf-8') as f:
content = f.read()
if 'api.anthropic.com' in content:
print("WARNING: Still contains references to api.anthropic.com")
else:
print("VERIFIED: No more direct API calls")
if '/api/llm/generate' in content:
print("VERIFIED: Now using Flask backend")
print("\nDone! Please:")
print("1. Restart Flask: python app_updated.py")
print("2. Reload browser: Ctrl+F5")
print("3. Test AI generation")

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 讀取原始文件
with open('excel.md', 'r', encoding='utf-8') as f:
lines = f.readlines()
# 跳過表頭,處理數據
data_lines = lines[1:] # 跳過第一行表頭
result = []
current_values = {
'事業體': '',
'處級單位': '',
'部級單位': '',
}
for line in data_lines:
line = line.rstrip('\n\r')
if not line.strip():
continue
# 分割 Tab
parts = line.split('\t')
# 確保至少有4個元素
while len(parts) < 4:
parts.append('')
# 處理每一列
# 第1列事業體
if parts[0].strip():
current_values['事業體'] = parts[0].strip()
# 第2列處別處級單位
if parts[1].strip():
current_values['處級單位'] = parts[1].strip()
# 第3列單位名稱可能是處級、部級或其他
if parts[2].strip():
unit_name = parts[2].strip()
# 判斷單位級別
if unit_name.endswith(''):
# 如果是處級單位,更新處級單位,清空部級單位
current_values['處級單位'] = unit_name
current_values['部級單位'] = ''
elif unit_name.endswith(''):
# 如果是部級單位,更新部級單位
current_values['部級單位'] = unit_name
elif '' in unit_name:
# 如果包含"部",視為部級單位
current_values['部級單位'] = unit_name
elif '' in unit_name:
# 如果包含"處",視為處級單位
current_values['處級單位'] = unit_name
current_values['部級單位'] = ''
# 第4列崗位名稱
position_name = parts[3].strip() if len(parts) > 3 else ''
# 如果沒有崗位名稱,嘗試從其他列找(可能崗位名稱在其他位置)
if not position_name:
# 從後往前找第一個非空值作為崗位名稱
for i in range(len(parts) - 1, -1, -1):
if parts[i].strip() and i != 0 and i != 1 and i != 2:
position_name = parts[i].strip()
break
# 只有當有崗位名稱時才加入結果
if position_name:
result.append([
current_values['事業體'],
current_values['處級單位'],
current_values['部級單位'],
position_name
])
# 生成 Markdown 表格
output = []
output.append('| 事業體 | 處級單位 | 部級單位 | 崗位名稱 |')
output.append('|--------|----------|----------|----------|')
for row in result:
# 轉義管道符號
row_escaped = [cell.replace('|', '\\|') if cell else '' for cell in row]
output.append(f"| {row_escaped[0]} | {row_escaped[1]} | {row_escaped[2]} | {row_escaped[3]} |")
# 寫入新文件
with open('excel_table.md', 'w', encoding='utf-8') as f:
f.write('\n'.join(output))
print(f"轉換完成!共生成 {len(result)} 行數據。")
print("輸出文件excel_table.md")

View File

@@ -0,0 +1,78 @@
"""
從 excel_table copy.md 提取下拉選單資料
"""
import re
from collections import OrderedDict
# 讀取文件
with open('excel_table copy.md', 'r', encoding='utf-8') as f:
content = f.read()
# 解析表格
lines = content.strip().split('\n')
data = []
for line in lines[2:]: # 跳過標題和分隔線
if line.strip():
# 使用正則表達式分割,處理可能的空白單元格
cols = [col.strip() for col in line.split('|')[1:-1]] # 去掉首尾的空字符串
if len(cols) == 4:
data.append({
'事業體': cols[0],
'處級單位': cols[1],
'部級單位': cols[2],
'崗位名稱': cols[3]
})
# 提取唯一值並保持順序
business_units = list(OrderedDict.fromkeys([d['事業體'] for d in data if d['事業體']]))
dept_level1 = list(OrderedDict.fromkeys([d['處級單位'] for d in data if d['處級單位']]))
dept_level2 = list(OrderedDict.fromkeys([d['部級單位'] for d in data if d['部級單位']]))
position_names = list(OrderedDict.fromkeys([d['崗位名稱'] for d in data if d['崗位名稱']]))
print("=" * 80)
print("資料統計")
print("=" * 80)
print(f"總資料筆數: {len(data)}")
print(f"事業體數量: {len(business_units)}")
print(f"處級單位數量: {len(dept_level1)}")
print(f"部級單位數量: {len(dept_level2)}")
print(f"崗位名稱數量: {len(position_names)}")
print()
# 生成 JavaScript 數組
js_business_units = f"const businessUnits = {business_units};"
js_dept_level1 = f"const deptLevel1Units = {dept_level1};"
js_dept_level2 = f"const deptLevel2Units = {dept_level2};"
js_position_names = f"const positionNames = {position_names};"
print("=" * 80)
print("JavaScript 數組 (複製以下內容)")
print("=" * 80)
print()
print("// 事業體")
print(js_business_units)
print()
print("// 處級單位")
print(js_dept_level1)
print()
print("// 部級單位")
print(js_dept_level2)
print()
print("// 崗位名稱")
print(js_position_names)
print()
# 儲存到文件
with open('dropdown_data.js', 'w', encoding='utf-8') as f:
f.write("// 自動生成的下拉選單資料\n\n")
f.write("// 事業體\n")
f.write(js_business_units + "\n\n")
f.write("// 處級單位\n")
f.write(js_dept_level1 + "\n\n")
f.write("// 部級單位\n")
f.write(js_dept_level2 + "\n\n")
f.write("// 崗位名稱\n")
f.write(js_position_names + "\n")
print("已儲存到 dropdown_data.js")

View File

@@ -0,0 +1,123 @@
"""
從 excel_table copy.md 提取階層式關聯資料
用於實現下拉選單的連動功能
"""
import re
import json
from collections import OrderedDict, defaultdict
# 讀取文件
with open('excel_table copy.md', 'r', encoding='utf-8') as f:
content = f.read()
# 解析表格
lines = content.strip().split('\n')
data = []
for line in lines[2:]: # 跳過標題和分隔線
if line.strip():
# 使用正則表達式分割,處理可能的空白單元格
cols = [col.strip() for col in line.split('|')[1:-1]] # 去掉首尾的空字符串
if len(cols) == 4:
data.append({
'事業體': cols[0],
'處級單位': cols[1],
'部級單位': cols[2],
'崗位名稱': cols[3]
})
# 建立階層關聯
# 事業體 -> 處級單位的對應
business_to_division = defaultdict(set)
# 處級單位 -> 部級單位的對應
division_to_department = defaultdict(set)
# 部級單位 -> 崗位名稱的對應
department_to_position = defaultdict(set)
# 也建立完整的階層路徑
full_hierarchy = []
for row in data:
business = row['事業體']
division = row['處級單位']
department = row['部級單位']
position = row['崗位名稱']
if business and division:
business_to_division[business].add(division)
if division and department:
division_to_department[division].add(department)
if department and position:
department_to_position[department].add(position)
# 記錄完整路徑
if business and division and department and position:
full_hierarchy.append({
'business': business,
'division': division,
'department': department,
'position': position
})
# 轉換為列表並排序
def convert_to_sorted_list(d):
return {k: sorted(list(v)) for k, v in d.items()}
business_to_division_dict = convert_to_sorted_list(business_to_division)
division_to_department_dict = convert_to_sorted_list(division_to_department)
department_to_position_dict = convert_to_sorted_list(department_to_position)
# 統計資訊
print("=" * 80)
print("階層關聯統計")
print("=" * 80)
print(f"事業體數量: {len(business_to_division_dict)}")
print(f"處級單位數量: {len(division_to_department_dict)}")
print(f"部級單位數量: {len(department_to_position_dict)}")
print(f"完整階層路徑數量: {len(full_hierarchy)}")
print()
# 顯示幾個範例
print("範例關聯:")
print("-" * 80)
for business, divisions in list(business_to_division_dict.items())[:3]:
print(f"事業體: {business}")
print(f" -> 處級單位: {divisions}")
print()
# 生成 JavaScript 物件
js_code = """// 自動生成的階層關聯資料
// 事業體 -> 處級單位的對應
const businessToDivision = """
js_code += json.dumps(business_to_division_dict, ensure_ascii=False, indent=2)
js_code += """;
// 處級單位 -> 部級單位的對應
const divisionToDepartment = """
js_code += json.dumps(division_to_department_dict, ensure_ascii=False, indent=2)
js_code += """;
// 部級單位 -> 崗位名稱的對應
const departmentToPosition = """
js_code += json.dumps(department_to_position_dict, ensure_ascii=False, indent=2)
js_code += """;
// 完整階層資料(用於反向查詢)
const fullHierarchyData = """
js_code += json.dumps(full_hierarchy, ensure_ascii=False, indent=2)
js_code += ";\n"
# 儲存到文件
with open('hierarchical_data.js', 'w', encoding='utf-8') as f:
f.write(js_code)
print("=" * 80)
print("已儲存到 hierarchical_data.js")
print("=" * 80)

53
scripts/fix_csv_routes.py Normal file
View File

@@ -0,0 +1,53 @@
"""
修復 app_updated.py 中重複的 CSV 路由
"""
import re
# 讀取檔案
with open('app_updated.py', 'r', encoding='utf-8') as f:
content = f.read()
# 找到並刪除重複的 CSV 匯入/匯出 API 區塊 (第 852 行開始)
# 保留第一次定義(已經移到正確位置的),刪除後面的重複定義
# 找到 "# ==================== CSV 匯入/匯出 API ====================" 的位置
csv_section_pattern = r'# ====================CSV匯入/匯出 API ====================.*?(?=# ====================)'
# 刪除重複的 CSV 區塊 (保留第一次定義)
lines = content.split('\n')
new_lines = []
skip_until_next_section = False
first_csv_section_found = False
i = 0
while i < len(lines):
line = lines[i]
# 檢查是否是 CSV 匯入/匯出 API 區段
if '# ==================== CSV 匯入/匯出 API ====================' in line:
if not first_csv_section_found:
# 第一次遇到,跳過這個區塊(因為我們已經在前面定義了)
first_csv_section_found = True
skip_until_next_section = True
else:
# 第二次遇到重複區塊,跳過
skip_until_next_section = True
# 檢查是否遇到下一個區段
if skip_until_next_section and '# ====================' in line and 'CSV 匯入/匯出' not in line:
skip_until_next_section = False
new_lines.append(line)
i += 1
continue
if not skip_until_next_section:
new_lines.append(line)
i += 1
# 寫回檔案
with open('app_updated.py', 'w', encoding='utf-8') as f:
f.write('\n'.join(new_lines))
print("已修復 CSV 路由重複問題")

View File

@@ -0,0 +1,90 @@
"""
修正 Gemini 模型名稱和關閉按鈕
"""
# 1. 修正 llm_config.py 中的 Gemini 模型
with open('llm_config.py', 'r', encoding='utf-8') as f:
content = f.read()
# 備份
with open('llm_config.py.backup', 'w', encoding='utf-8') as f:
f.write(content)
# 替換模型名稱gemini-pro -> gemini-2.0-flash-exp
old_model = 'gemini-pro'
new_model = 'gemini-2.0-flash-exp' # 使用最新的 Gemini 2.0 Flash
content = content.replace(
f'models/{old_model}:generateContent',
f'models/{new_model}:generateContent'
)
if 'gemini-2.0-flash-exp' in content:
print(f"SUCCESS: Updated Gemini model to {new_model}")
else:
print("ERROR: Could not update model")
with open('llm_config.py', 'w', encoding='utf-8') as f:
f.write(content)
# 2. 修正 index.html 中的關閉按鈕
with open('index.html', 'r', encoding='utf-8') as f:
html_content = f.read()
# 備份
with open('index.html.backup3', 'w', encoding='utf-8') as f:
f.write(html_content)
# 找到並修正關閉按鈕
# 問題onclick 使用了複雜的選擇器,可能失效
# 解決:使用更簡單可靠的方式
old_close_button = '''<button onclick="this.closest('[style*=\\'position: fixed\\']').remove()"'''
new_close_button = '''<button onclick="closeErrorModal(this)"'''
if old_close_button in html_content:
html_content = html_content.replace(old_close_button, new_close_button)
print("SUCCESS: Updated close button onclick")
else:
print("INFO: Close button pattern not found (might be already fixed)")
# 添加 closeErrorModal 函數(如果不存在)
if 'function closeErrorModal' not in html_content:
close_modal_function = '''
// 關閉錯誤訊息對話框
function closeErrorModal(button) {
const modal = button.closest('[style*="position: fixed"]');
if (modal) {
modal.style.opacity = '0';
setTimeout(() => modal.remove(), 300);
}
}
'''
# 在 showCopyableError 函數後添加
if 'function showCopyableError' in html_content:
html_content = html_content.replace(
' // 複製錯誤訊息到剪貼板',
close_modal_function + '\n // 複製錯誤訊息到剪貼板'
)
print("SUCCESS: Added closeErrorModal function")
# 同時修正底部的確定按鈕
old_confirm_button = '''<button onclick="this.closest('[style*=\\'position: fixed\\']').remove()"'''
if old_confirm_button in html_content:
html_content = html_content.replace(old_confirm_button, new_close_button)
print("SUCCESS: Updated confirm button onclick")
with open('index.html', 'w', encoding='utf-8') as f:
f.write(html_content)
print("\nAll fixes applied!")
print("\nChanges made:")
print(f"1. Gemini model: {old_model} -> {new_model}")
print("2. Close button: Fixed onclick handler")
print("3. Added closeErrorModal function")
print("\nNext steps:")
print("1. Restart Flask server")
print("2. Reload browser page (Ctrl+F5)")
print("3. Test AI generation")

382
scripts/generate_review.py Normal file
View File

@@ -0,0 +1,382 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import json
# 讀取表格數據
with open('excel_table.md', 'r', encoding='utf-8') as f:
lines = f.readlines()
# 解析數據(跳過表頭和分隔線)
data = []
for line in lines[2:]: # 跳過表頭和分隔線
line = line.strip()
if not line or not line.startswith('|'):
continue
# 移除首尾的管道符號並分割
parts = [p.strip() for p in line[1:-1].split('|')]
if len(parts) >= 4:
data.append({
'事業體': parts[0],
'處級單位': parts[1],
'部級單位': parts[2],
'崗位名稱': parts[3]
})
# 生成 HTML
html_content = '''<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>組織架構預覽</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Microsoft JhengHei", "微軟正黑體", Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
min-height: 100vh;
}
/* Header using Float */
.header {
background: rgba(255, 255, 255, 0.95);
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden; /* Clear float */
}
.header h1 {
float: left;
color: #333;
font-size: 28px;
}
.header .stats {
float: right;
color: #666;
font-size: 14px;
padding-top: 8px;
}
.header::after {
content: "";
display: table;
clear: both;
}
/* Filter Section using Float */
.filters {
background: rgba(255, 255, 255, 0.95);
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.filter-group {
float: left;
margin-right: 20px;
margin-bottom: 10px;
}
.filter-group label {
display: block;
margin-bottom: 5px;
color: #555;
font-weight: bold;
font-size: 14px;
}
.filter-group select,
.filter-group input {
padding: 8px 12px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 14px;
width: 200px;
}
.filter-group select:focus,
.filter-group input:focus {
outline: none;
border-color: #667eea;
}
.filters::after {
content: "";
display: table;
clear: both;
}
/* Table Container using Float */
.table-container {
background: rgba(255, 255, 255, 0.95);
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
thead {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
th {
padding: 15px;
text-align: left;
font-weight: bold;
position: sticky;
top: 0;
z-index: 10;
}
tbody tr {
border-bottom: 1px solid #eee;
transition: background-color 0.2s;
}
tbody tr:hover {
background-color: #f5f5f5;
}
tbody tr:nth-child(even) {
background-color: #fafafa;
}
tbody tr:nth-child(even):hover {
background-color: #f0f0f0;
}
td {
padding: 12px 15px;
color: #333;
}
td:first-child {
font-weight: 600;
color: #667eea;
}
td:last-child {
color: #764ba2;
font-weight: 500;
}
/* Empty cells styling */
td:empty::before {
content: "";
color: #ccc;
}
/* Footer using Float */
.footer {
margin-top: 20px;
background: rgba(255, 255, 255, 0.95);
padding: 15px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
text-align: center;
color: #666;
font-size: 14px;
}
/* Responsive Design */
@media (max-width: 768px) {
.filter-group {
float: none;
width: 100%;
margin-right: 0;
}
.filter-group select,
.filter-group input {
width: 100%;
}
.header h1 {
float: none;
margin-bottom: 10px;
}
.header .stats {
float: none;
}
}
</style>
</head>
<body>
<div class="header">
<h1>📊 公司組織架構預覽</h1>
<div class="stats">總計: <span id="totalCount">''' + str(len(data)) + '''</span> 筆資料</div>
</div>
<div class="filters">
<div class="filter-group">
<label for="filterBusiness">事業體篩選</label>
<select id="filterBusiness">
<option value="">全部</option>
</select>
</div>
<div class="filter-group">
<label for="filterDepartment">處級單位篩選</label>
<select id="filterDepartment">
<option value="">全部</option>
</select>
</div>
<div class="filter-group">
<label for="filterDivision">部級單位篩選</label>
<select id="filterDivision">
<option value="">全部</option>
</select>
</div>
<div class="filter-group">
<label for="searchPosition">崗位搜尋</label>
<input type="text" id="searchPosition" placeholder="輸入崗位名稱...">
</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>事業體</th>
<th>處級單位</th>
<th>部級單位</th>
<th>崗位名稱</th>
</tr>
</thead>
<tbody id="tableBody">
'''
# 添加表格行
for row in data:
html_content += f''' <tr>
<td>{row['事業體']}</td>
<td>{row['處級單位'] if row['處級單位'] else ''}</td>
<td>{row['部級單位'] if row['部級單位'] else ''}</td>
<td>{row['崗位名稱']}</td>
</tr>
'''
html_content += ''' </tbody>
</table>
</div>
<div class="footer">
<p>組織架構資料預覽系統 | 使用 CSS Float Layout 設計</p>
</div>
<script>
// 獲取所有數據
const allData = ''' + json.dumps(data, ensure_ascii=False) + ''';
// 獲取唯一的選項值
function getUniqueValues(key) {
const values = new Set();
allData.forEach(row => {
if (row[key]) {
values.add(row[key]);
}
});
return Array.from(values).sort();
}
// 填充下拉選單
function populateSelects() {
const businessSelect = document.getElementById('filterBusiness');
const deptSelect = document.getElementById('filterDepartment');
const divSelect = document.getElementById('filterDivision');
getUniqueValues('事業體').forEach(value => {
const option = document.createElement('option');
option.value = value;
option.textContent = value;
businessSelect.appendChild(option);
});
getUniqueValues('處級單位').forEach(value => {
const option = document.createElement('option');
option.value = value;
option.textContent = value;
deptSelect.appendChild(option);
});
getUniqueValues('部級單位').forEach(value => {
const option = document.createElement('option');
option.value = value;
option.textContent = value;
divSelect.appendChild(option);
});
}
// 過濾數據
function filterData() {
const businessFilter = document.getElementById('filterBusiness').value;
const deptFilter = document.getElementById('filterDepartment').value;
const divFilter = document.getElementById('filterDivision').value;
const positionSearch = document.getElementById('searchPosition').value.toLowerCase();
const filtered = allData.filter(row => {
const matchBusiness = !businessFilter || row['事業體'] === businessFilter;
const matchDept = !deptFilter || row['處級單位'] === deptFilter;
const matchDiv = !divFilter || row['部級單位'] === divFilter;
const matchPosition = !positionSearch || row['崗位名稱'].toLowerCase().includes(positionSearch);
return matchBusiness && matchDept && matchDiv && matchPosition;
});
renderTable(filtered);
document.getElementById('totalCount').textContent = filtered.length;
}
// 渲染表格
function renderTable(data) {
const tbody = document.getElementById('tableBody');
tbody.innerHTML = '';
data.forEach(row => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${row['事業體'] || ''}</td>
<td>${row['處級單位'] || ''}</td>
<td>${row['部級單位'] || ''}</td>
<td>${row['崗位名稱'] || ''}</td>
`;
tbody.appendChild(tr);
});
}
// 事件監聽
document.getElementById('filterBusiness').addEventListener('change', filterData);
document.getElementById('filterDepartment').addEventListener('change', filterData);
document.getElementById('filterDivision').addEventListener('change', filterData);
document.getElementById('searchPosition').addEventListener('input', filterData);
// 初始化
populateSelects();
</script>
</body>
</html>'''
# 寫入文件
with open('review.html', 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"預覽頁面已生成review.html")
print(f"共包含 {len(data)} 筆組織架構資料")

View File

@@ -0,0 +1,387 @@
"""
匯入組織階層資料到資料庫
從 hierarchical_data.js 讀取資料並匯入 MySQL
"""
import os
import re
import json
import pymysql
from dotenv import load_dotenv
from datetime import datetime
# Load environment variables
load_dotenv()
# 從 hierarchical_data.js 解析資料
def parse_hierarchical_data():
"""解析 hierarchical_data.js 檔案"""
js_file = os.path.join(os.path.dirname(__file__), 'hierarchical_data.js')
with open(js_file, 'r', encoding='utf-8') as f:
content = f.read()
# 提取 businessToDivision
business_match = re.search(r'const businessToDivision = ({[\s\S]*?});', content)
business_to_division = json.loads(business_match.group(1).replace("'", '"')) if business_match else {}
# 提取 divisionToDepartment
division_match = re.search(r'const divisionToDepartment = ({[\s\S]*?});', content)
division_to_department = json.loads(division_match.group(1).replace("'", '"')) if division_match else {}
# 提取 departmentToPosition
dept_match = re.search(r'const departmentToPosition = ({[\s\S]*?});', content)
department_to_position = json.loads(dept_match.group(1).replace("'", '"')) if dept_match else {}
# 提取 fullHierarchyData
hierarchy_match = re.search(r'const fullHierarchyData = (\[[\s\S]*?\]);', content)
full_hierarchy_data = json.loads(hierarchy_match.group(1)) if hierarchy_match else []
return {
'businessToDivision': business_to_division,
'divisionToDepartment': division_to_department,
'departmentToPosition': department_to_position,
'fullHierarchyData': full_hierarchy_data
}
def generate_code(prefix, index):
"""生成代碼"""
return f"{prefix}{index:03d}"
def import_to_database():
"""匯入資料到資料庫"""
# Database connection parameters
db_config = {
'host': os.getenv('DB_HOST', 'localhost'),
'port': int(os.getenv('DB_PORT', 3306)),
'user': os.getenv('DB_USER', 'root'),
'password': os.getenv('DB_PASSWORD', ''),
'database': os.getenv('DB_NAME', 'hr_position_system'),
'charset': 'utf8mb4',
'cursorclass': pymysql.cursors.DictCursor
}
print("=" * 60)
print("組織階層資料匯入工具")
print("=" * 60)
print()
# 解析 JS 資料
print("步驟 1: 解析 hierarchical_data.js...")
data = parse_hierarchical_data()
print(f" - 事業體數量: {len(data['businessToDivision'])}")
print(f" - 處級單位對應數: {len(data['divisionToDepartment'])}")
print(f" - 部級單位對應數: {len(data['departmentToPosition'])}")
print(f" - 完整階層記錄數: {len(data['fullHierarchyData'])}")
print()
try:
# 連接資料庫
print("步驟 2: 連接資料庫...")
connection = pymysql.connect(**db_config)
cursor = connection.cursor()
print(" 連接成功")
print()
# 先建立資料表(如果不存在)
print("步驟 3: 確認資料表存在...")
create_tables_sql = """
-- 事業體表
CREATE TABLE IF NOT EXISTS business_units (
id INT AUTO_INCREMENT PRIMARY KEY,
business_code VARCHAR(20) NOT NULL UNIQUE,
business_name VARCHAR(100) NOT NULL,
sort_order INT DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
remark VARCHAR(500),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_business_name (business_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 處級單位表
CREATE TABLE IF NOT EXISTS divisions (
id INT AUTO_INCREMENT PRIMARY KEY,
division_code VARCHAR(20) NOT NULL UNIQUE,
division_name VARCHAR(100) NOT NULL,
business_id INT,
sort_order INT DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
remark VARCHAR(500),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_division_name (division_name),
INDEX idx_business_id (business_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 部級單位表
CREATE TABLE IF NOT EXISTS departments (
id INT AUTO_INCREMENT PRIMARY KEY,
department_code VARCHAR(20) NOT NULL UNIQUE,
department_name VARCHAR(100) NOT NULL,
division_id INT,
sort_order INT DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
remark VARCHAR(500),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_department_name (department_name),
INDEX idx_division_id (division_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 組織崗位關聯表
CREATE TABLE IF NOT EXISTS organization_positions (
id INT AUTO_INCREMENT PRIMARY KEY,
business_id INT NOT NULL,
division_id INT NOT NULL,
department_id INT NOT NULL,
position_title VARCHAR(100) NOT NULL,
sort_order INT DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_business_id (business_id),
INDEX idx_division_id (division_id),
INDEX idx_department_id (department_id),
INDEX idx_position_title (position_title),
UNIQUE KEY uk_org_position (business_id, division_id, department_id, position_title)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
"""
for statement in create_tables_sql.split(';'):
if statement.strip():
try:
cursor.execute(statement)
except Exception as e:
pass # 表已存在時忽略錯誤
connection.commit()
print(" 資料表已確認")
print()
# 清空現有資料
print("步驟 4: 清空現有資料...")
cursor.execute("SET FOREIGN_KEY_CHECKS = 0")
cursor.execute("TRUNCATE TABLE organization_positions")
cursor.execute("TRUNCATE TABLE departments")
cursor.execute("TRUNCATE TABLE divisions")
cursor.execute("TRUNCATE TABLE business_units")
cursor.execute("SET FOREIGN_KEY_CHECKS = 1")
connection.commit()
print(" 資料已清空")
print()
# 匯入事業體
print("步驟 5: 匯入事業體...")
business_id_map = {}
business_list = list(data['businessToDivision'].keys())
for idx, business_name in enumerate(business_list, 1):
code = generate_code('BU', idx)
cursor.execute(
"INSERT INTO business_units (business_code, business_name, sort_order) VALUES (%s, %s, %s)",
(code, business_name, idx)
)
business_id_map[business_name] = cursor.lastrowid
connection.commit()
print(f" 匯入 {len(business_list)} 筆事業體")
# 匯入處級單位
print("步驟 6: 匯入處級單位...")
division_id_map = {}
division_idx = 0
for business_name, divisions in data['businessToDivision'].items():
business_id = business_id_map.get(business_name)
for division_name in divisions:
if division_name not in division_id_map:
division_idx += 1
code = generate_code('DIV', division_idx)
cursor.execute(
"INSERT INTO divisions (division_code, division_name, business_id, sort_order) VALUES (%s, %s, %s, %s)",
(code, division_name, business_id, division_idx)
)
division_id_map[division_name] = cursor.lastrowid
connection.commit()
print(f" 匯入 {division_idx} 筆處級單位")
# 匯入部級單位
print("步驟 7: 匯入部級單位...")
department_id_map = {}
dept_idx = 0
for division_name, departments in data['divisionToDepartment'].items():
division_id = division_id_map.get(division_name)
for dept_name in departments:
if dept_name not in department_id_map:
dept_idx += 1
code = generate_code('DEPT', dept_idx)
cursor.execute(
"INSERT INTO departments (department_code, department_name, division_id, sort_order) VALUES (%s, %s, %s, %s)",
(code, dept_name, division_id, dept_idx)
)
department_id_map[dept_name] = cursor.lastrowid
connection.commit()
print(f" 匯入 {dept_idx} 筆部級單位")
# 匯入組織崗位關聯
print("步驟 8: 匯入組織崗位關聯...")
position_count = 0
inserted_combinations = set()
for record in data['fullHierarchyData']:
business_name = record.get('business')
division_name = record.get('division')
department_name = record.get('department')
position_title = record.get('position')
business_id = business_id_map.get(business_name)
division_id = division_id_map.get(division_name)
department_id = department_id_map.get(department_name)
if not all([business_id, division_id, department_id, position_title]):
continue
# 避免重複插入
combination_key = (business_id, division_id, department_id, position_title)
if combination_key in inserted_combinations:
continue
inserted_combinations.add(combination_key)
try:
cursor.execute(
"""INSERT INTO organization_positions
(business_id, division_id, department_id, position_title, sort_order)
VALUES (%s, %s, %s, %s, %s)""",
(business_id, division_id, department_id, position_title, position_count + 1)
)
position_count += 1
except pymysql.err.IntegrityError:
pass # 重複記錄跳過
connection.commit()
print(f" 匯入 {position_count} 筆組織崗位關聯")
print()
# 顯示統計
print("=" * 60)
print("匯入完成!")
print("=" * 60)
print()
# 查詢統計
cursor.execute("SELECT COUNT(*) as cnt FROM business_units")
business_count = cursor.fetchone()['cnt']
cursor.execute("SELECT COUNT(*) as cnt FROM divisions")
division_count = cursor.fetchone()['cnt']
cursor.execute("SELECT COUNT(*) as cnt FROM departments")
dept_count = cursor.fetchone()['cnt']
cursor.execute("SELECT COUNT(*) as cnt FROM organization_positions")
org_pos_count = cursor.fetchone()['cnt']
print(f"資料庫統計:")
print(f" - 事業體: {business_count}")
print(f" - 處級單位: {division_count}")
print(f" - 部級單位: {dept_count}")
print(f" - 組織崗位關聯: {org_pos_count}")
cursor.close()
connection.close()
return True
except Exception as e:
print(f"\n錯誤: {str(e)}")
return False
def import_to_memory():
"""匯入資料到記憶體(用於 Flask 應用)"""
# 解析 JS 資料
data = parse_hierarchical_data()
# 建立記憶體資料結構
business_units = {}
divisions = {}
departments = {}
organization_positions = []
business_idx = 0
division_idx = 0
dept_idx = 0
# 處理事業體
for business_name in data['businessToDivision'].keys():
business_idx += 1
business_units[business_name] = {
'id': business_idx,
'code': generate_code('BU', business_idx),
'name': business_name
}
# 處理處級單位
division_id_map = {}
for business_name, division_list in data['businessToDivision'].items():
for div_name in division_list:
if div_name not in division_id_map:
division_idx += 1
division_id_map[div_name] = division_idx
divisions[div_name] = {
'id': division_idx,
'code': generate_code('DIV', division_idx),
'name': div_name,
'business': business_name
}
# 處理部級單位
dept_id_map = {}
for div_name, dept_list in data['divisionToDepartment'].items():
for dept_name in dept_list:
if dept_name not in dept_id_map:
dept_idx += 1
dept_id_map[dept_name] = dept_idx
departments[dept_name] = {
'id': dept_idx,
'code': generate_code('DEPT', dept_idx),
'name': dept_name,
'division': div_name
}
# 處理組織崗位關聯
seen = set()
for record in data['fullHierarchyData']:
key = (record['business'], record['division'], record['department'], record['position'])
if key not in seen:
seen.add(key)
organization_positions.append({
'business': record['business'],
'division': record['division'],
'department': record['department'],
'position': record['position']
})
return {
'business_units': business_units,
'divisions': divisions,
'departments': departments,
'organization_positions': organization_positions,
'businessToDivision': data['businessToDivision'],
'divisionToDepartment': data['divisionToDepartment'],
'departmentToPosition': data['departmentToPosition']
}
if __name__ == '__main__':
import_to_database()

View File

@@ -0,0 +1,252 @@
"""
改進錯誤訊息顯示 - 使錯誤訊息可完整顯示和複製
"""
with open('index.html', 'r', encoding='utf-8') as f:
content = f.read()
# 備份
with open('index.html.backup2', 'w', encoding='utf-8') as f:
f.write(content)
# 找到錯誤處理的 alert 並替換為更好的顯示方式
old_error_handling = ''' } catch (error) {
console.error("Error calling LLM API:", error);
alert(`AI 生成錯誤: ${error.message}\\n\\n請確保\\n1. Flask 後端已啟動 (python app_updated.py)\\n2. 已在 .env 文件中配置 LLM API Key\\n3. 網路連線正常`);
throw error;
}'''
new_error_handling = ''' } catch (error) {
console.error("Error calling LLM API:", error);
// 嘗試解析更詳細的錯誤訊息
let errorDetails = error.message;
try {
// 如果錯誤訊息是 JSON 格式,嘗試美化顯示
const errorJson = JSON.parse(error.message);
errorDetails = JSON.stringify(errorJson, null, 2);
} catch (e) {
// 不是 JSON使用原始訊息
}
// 創建可複製的錯誤對話框
showCopyableError({
title: 'AI 生成錯誤',
message: error.message,
details: errorDetails,
suggestions: [
'Flask 後端已啟動 (python start_server.py)',
'已在 .env 文件中配置有效的 LLM API Key',
'網路連線正常',
'嘗試使用不同的 LLM API (DeepSeek 或 OpenAI)'
]
});
throw error;
}'''
# 替換
new_content = content.replace(old_error_handling, new_error_handling)
if new_content == content:
print("WARNING: Pattern not found, content not changed")
else:
print("SUCCESS: Error handling improved")
# 添加 showCopyableError 函數(如果還沒有)
if 'function showCopyableError' not in new_content:
# 在 </script> 前添加新函數
error_display_function = '''
// 顯示可複製的錯誤訊息
function showCopyableError(options) {
const { title, message, details, suggestions } = options;
// 創建對話框
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
animation: fadeIn 0.3s;
`;
modal.innerHTML = `
<div style="
background: white;
border-radius: 12px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
">
<!-- Header -->
<div style="
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
color: white;
padding: 20px;
display: flex;
align-items: center;
gap: 15px;
">
<span style="font-size: 2rem;">❌</span>
<h3 style="margin: 0; font-size: 1.3rem; flex: 1;">${title}</h3>
<button onclick="this.closest('[style*=\\'position: fixed\\']').remove()" style="
background: rgba(255,255,255,0.2);
border: none;
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
font-size: 1.2rem;
display: flex;
align-items: center;
justify-content: center;
">×</button>
</div>
<!-- Body -->
<div style="
padding: 25px;
overflow-y: auto;
flex: 1;
">
<div style="
color: #333;
line-height: 1.6;
margin-bottom: 20px;
font-size: 1rem;
">${message}</div>
${suggestions && suggestions.length > 0 ? `
<div style="
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 6px;
padding: 15px;
margin-bottom: 20px;
">
<strong style="color: #856404; display: block; margin-bottom: 10px;">💡 請確保:</strong>
<ul style="margin: 0; padding-left: 20px; color: #856404;">
${suggestions.map(s => `<li style="margin: 5px 0;">${s}</li>`).join('')}
</ul>
</div>
` : ''}
${details ? `
<details style="
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 15px;
">
<summary style="
cursor: pointer;
font-weight: 600;
color: #495057;
user-select: none;
margin-bottom: 10px;
">🔍 詳細錯誤訊息(點擊展開)</summary>
<pre id="errorDetailsText" style="
background: white;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
font-size: 0.85rem;
color: #666;
margin: 10px 0 0 0;
white-space: pre-wrap;
word-break: break-word;
max-height: 300px;
overflow-y: auto;
">${details}</pre>
<button onclick="copyErrorDetails()" style="
background: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
font-size: 0.9rem;
">📋 複製錯誤訊息</button>
</details>
` : ''}
</div>
<!-- Footer -->
<div style="
padding: 15px 25px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: flex-end;
gap: 10px;
">
<button onclick="this.closest('[style*=\\'position: fixed\\']').remove()" style="
background: #007bff;
color: white;
border: none;
padding: 10px 25px;
border-radius: 6px;
cursor: pointer;
font-size: 0.95rem;
font-weight: 500;
">確定</button>
</div>
</div>
`;
document.body.appendChild(modal);
// 點擊背景關閉
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
}
});
}
// 複製錯誤訊息到剪貼板
function copyErrorDetails() {
const text = document.getElementById('errorDetailsText').textContent;
navigator.clipboard.writeText(text).then(() => {
alert('錯誤訊息已複製到剪貼板!');
}).catch(err => {
// Fallback: 選取文字
const range = document.createRange();
range.selectNode(document.getElementById('errorDetailsText'));
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
try {
document.execCommand('copy');
alert('錯誤訊息已複製到剪貼板!');
} catch (e) {
alert('複製失敗,請手動選取並複製');
}
});
}
'''
new_content = new_content.replace(' </script>', error_display_function + '\n </script>')
print("Added showCopyableError function")
# 寫回
with open('index.html', 'w', encoding='utf-8') as f:
f.write(new_content)
print("\nDone! Improvements:")
print("1. Error messages now show in a modal dialog")
print("2. Full error details are expandable")
print("3. Error details can be copied to clipboard")
print("4. Better formatting and readability")
print("\nPlease reload the page (Ctrl+F5) to see the changes")

130
scripts/init_database.py Normal file
View File

@@ -0,0 +1,130 @@
"""
Database initialization script for HR Position System
Connects to MySQL and executes the schema creation script
"""
import os
import pymysql
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
def init_database():
"""Initialize the database schema"""
# Database connection parameters
db_config = {
'host': os.getenv('DB_HOST'),
'port': int(os.getenv('DB_PORT')),
'user': os.getenv('DB_USER'),
'password': os.getenv('DB_PASSWORD'),
'database': os.getenv('DB_NAME'),
'charset': 'utf8mb4',
'cursorclass': pymysql.cursors.DictCursor
}
print(f"Connecting to database: {db_config['host']}:{db_config['port']}")
print(f"Database: {db_config['database']}")
try:
# Connect to MySQL
connection = pymysql.connect(**db_config)
print("✓ Successfully connected to database")
# Read SQL schema file
schema_file = os.path.join(os.path.dirname(__file__), 'database_schema.sql')
with open(schema_file, 'r', encoding='utf-8') as f:
sql_script = f.read()
# Split SQL script by semicolon and execute each statement
cursor = connection.cursor()
# Split by semicolon and filter out empty statements
statements = [stmt.strip() for stmt in sql_script.split(';') if stmt.strip()]
print(f"\nExecuting {len(statements)} SQL statements...")
executed = 0
for i, statement in enumerate(statements, 1):
try:
# Skip comments and empty lines
if not statement or statement.startswith('--'):
continue
cursor.execute(statement)
executed += 1
# Show progress every 10 statements
if executed % 10 == 0:
print(f" Executed {executed} statements...")
except Exception as e:
print(f" Warning on statement {i}: {str(e)[:100]}")
continue
# Commit changes
connection.commit()
print(f"\n✓ Successfully executed {executed} SQL statements")
# Verify tables were created
cursor.execute("""
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'hr_position_system'
ORDER BY table_name
""")
tables = cursor.fetchall()
print(f"\n✓ Created {len(tables)} tables:")
for table in tables:
print(f" - {table['table_name']}")
# Close connection
cursor.close()
connection.close()
print("\n✓ Database initialization completed successfully!")
return True
except Exception as e:
print(f"\n✗ Error initializing database: {str(e)}")
return False
def test_connection():
"""Test database connection"""
db_config = {
'host': os.getenv('DB_HOST'),
'port': int(os.getenv('DB_PORT')),
'user': os.getenv('DB_USER'),
'password': os.getenv('DB_PASSWORD'),
'database': os.getenv('DB_NAME')
}
try:
connection = pymysql.connect(**db_config)
print("✓ Database connection test successful")
connection.close()
return True
except Exception as e:
print(f"✗ Database connection test failed: {str(e)}")
return False
if __name__ == '__main__':
print("=" * 60)
print("HR Position System - Database Initialization")
print("=" * 60)
print()
# Test connection first
print("Step 1: Testing database connection...")
if not test_connection():
print("\nPlease check your database configuration in .env file")
exit(1)
print("\nStep 2: Initializing database schema...")
if init_database():
print("\nDatabase is ready to use!")
else:
print("\nDatabase initialization failed. Please check the errors above.")
exit(1)

244
scripts/init_gitea.py Normal file
View File

@@ -0,0 +1,244 @@
"""
Gitea repository initialization script
Creates a new repository on Gitea server and sets up git remote
"""
import os
import requests
import subprocess
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
def create_gitea_repo():
"""Create a new repository on Gitea"""
gitea_url = os.getenv('GITEA_URL').rstrip('/')
gitea_token = os.getenv('GITEA_TOKEN')
gitea_user = os.getenv('GITEA_USER')
# Repository details
repo_name = 'hr-position-system'
repo_description = 'HR基礎資料維護系統 - 崗位與職務管理系統'
# API endpoint
api_url = f"{gitea_url}/api/v1/user/repos"
# Request headers
headers = {
'Authorization': f'token {gitea_token}',
'Content-Type': 'application/json'
}
# Repository data
data = {
'name': repo_name,
'description': repo_description,
'private': False,
'auto_init': False,
'default_branch': 'main'
}
print(f"Creating repository on Gitea: {gitea_url}")
print(f"Repository name: {repo_name}")
try:
# Create repository
response = requests.post(api_url, json=data, headers=headers)
if response.status_code == 201:
repo_info = response.json()
print(f"✓ Repository created successfully!")
print(f" Repository URL: {repo_info.get('html_url')}")
print(f" Clone URL (HTTPS): {repo_info.get('clone_url')}")
print(f" Clone URL (SSH): {repo_info.get('ssh_url')}")
return repo_info
elif response.status_code == 409:
print(f"✓ Repository '{repo_name}' already exists")
# Get existing repository info
repo_url = f"{gitea_url}/api/v1/repos/{gitea_user}/{repo_name}"
response = requests.get(repo_url, headers=headers)
if response.status_code == 200:
return response.json()
return None
else:
print(f"✗ Failed to create repository: {response.status_code}")
print(f" Error: {response.text}")
return None
except Exception as e:
print(f"✗ Error creating repository: {str(e)}")
return None
def init_git_local():
"""Initialize local git repository"""
repo_path = os.path.dirname(os.path.abspath(__file__))
try:
# Check if git is already initialized
git_dir = os.path.join(repo_path, '.git')
if os.path.exists(git_dir):
print("✓ Git repository already initialized")
return True
# Initialize git
subprocess.run(['git', 'init'], cwd=repo_path, check=True)
subprocess.run(['git', 'checkout', '-b', 'main'], cwd=repo_path, check=True)
print("✓ Git repository initialized")
return True
except Exception as e:
print(f"✗ Error initializing git: {str(e)}")
return False
def add_git_remote(repo_info):
"""Add Gitea remote to local repository"""
if not repo_info:
return False
repo_path = os.path.dirname(os.path.abspath(__file__))
clone_url = repo_info.get('clone_url')
try:
# Check if remote already exists
result = subprocess.run(
['git', 'remote', 'get-url', 'origin'],
cwd=repo_path,
capture_output=True,
text=True
)
if result.returncode == 0:
current_remote = result.stdout.strip()
if current_remote == clone_url:
print(f"✓ Remote 'origin' already configured: {clone_url}")
return True
else:
# Update remote URL
subprocess.run(
['git', 'remote', 'set-url', 'origin', clone_url],
cwd=repo_path,
check=True
)
print(f"✓ Remote 'origin' updated: {clone_url}")
return True
else:
# Add new remote
subprocess.run(
['git', 'remote', 'add', 'origin', clone_url],
cwd=repo_path,
check=True
)
print(f"✓ Remote 'origin' added: {clone_url}")
return True
except Exception as e:
print(f"✗ Error adding remote: {str(e)}")
return False
def create_initial_commit():
"""Create initial commit with project files"""
repo_path = os.path.dirname(os.path.abspath(__file__))
try:
# Check if there are already commits
result = subprocess.run(
['git', 'rev-parse', 'HEAD'],
cwd=repo_path,
capture_output=True,
text=True
)
if result.returncode == 0:
print("✓ Repository already has commits")
return True
# Add files
subprocess.run(['git', 'add', '.gitignore'], cwd=repo_path, check=True)
subprocess.run(['git', 'add', 'database_schema.sql'], cwd=repo_path, check=True)
subprocess.run(['git', 'add', 'init_database.py'], cwd=repo_path, check=True)
subprocess.run(['git', 'add', 'init_gitea.py'], cwd=repo_path, check=True)
subprocess.run(['git', 'add', 'SDD.md'], cwd=repo_path, check=True)
# Create initial commit
subprocess.run(
['git', 'commit', '-m', 'Initial commit: Project setup and database schema'],
cwd=repo_path,
check=True
)
print("✓ Initial commit created")
return True
except Exception as e:
print(f"✗ Error creating initial commit: {str(e)}")
return False
def test_gitea_connection():
"""Test connection to Gitea server"""
gitea_url = os.getenv('GITEA_URL').rstrip('/')
gitea_token = os.getenv('GITEA_TOKEN')
headers = {
'Authorization': f'token {gitea_token}'
}
try:
response = requests.get(f"{gitea_url}/api/v1/user", headers=headers)
if response.status_code == 200:
user_info = response.json()
print(f"✓ Gitea connection test successful")
print(f" User: {user_info.get('login')}")
print(f" Email: {user_info.get('email')}")
return True
else:
print(f"✗ Gitea connection test failed: {response.status_code}")
return False
except Exception as e:
print(f"✗ Gitea connection test failed: {str(e)}")
return False
if __name__ == '__main__':
print("=" * 60)
print("HR Position System - Gitea Repository Initialization")
print("=" * 60)
print()
# Test connection
print("Step 1: Testing Gitea connection...")
if not test_gitea_connection():
print("\nPlease check your Gitea configuration in .env file")
exit(1)
# Initialize local git
print("\nStep 2: Initializing local git repository...")
if not init_git_local():
exit(1)
# Create Gitea repository
print("\nStep 3: Creating Gitea repository...")
repo_info = create_gitea_repo()
if not repo_info:
exit(1)
# Add remote
print("\nStep 4: Configuring git remote...")
if not add_git_remote(repo_info):
exit(1)
# Create initial commit
print("\nStep 5: Creating initial commit...")
create_initial_commit()
print("\n" + "=" * 60)
print("✓ Gitea repository setup completed!")
print("=" * 60)
print("\nNext steps:")
print(" 1. Run: git push -u origin main")
print(" 2. Visit:", repo_info.get('html_url'))

110
scripts/quick_fix.py Normal file
View File

@@ -0,0 +1,110 @@
import re
with open('index.html', 'r', encoding='utf-8') as f:
content = f.read()
# 備份
with open('index.html.backup', 'w', encoding='utf-8') as f:
f.write(content)
print("Backup created: index.html.backup")
# 舊代碼
old = ''' async function callClaudeAPI(prompt) {
try {
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "claude-sonnet-4-20250514",
max_tokens: 2000,
messages: [
{ role: "user", content: prompt }
]
})
});
if (!response.ok) {
throw new Error(`API request failed: ${response.status}`);
}
const data = await response.json();
let responseText = data.content[0].text;
responseText = responseText.replace(/```json\\n?/g, "").replace(/```\\n?/g, "").trim();
return JSON.parse(responseText);
} catch (error) {
console.error("Error calling Claude API:", error);
throw error;
}
}'''
# 新代碼
new = ''' async function callClaudeAPI(prompt, api = 'gemini') {
try {
// 調用後端 Flask API避免 CORS 錯誤
const response = await fetch("/api/llm/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
api: api,
prompt: prompt,
max_tokens: 2000
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `API 請求失敗: ${response.status}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'API 調用失敗');
}
let responseText = data.text;
responseText = responseText.replace(/```json\\n?/g, "").replace(/```\\n?/g, "").trim();
return JSON.parse(responseText);
} catch (error) {
console.error("Error calling LLM API:", error);
alert(`AI 生成錯誤: ${error.message}\\n\\n請確保\\n1. Flask 後端已啟動 (python app_updated.py)\\n2. 已在 .env 文件中配置 LLM API Key\\n3. 網路連線正常`);
throw error;
}
}'''
# 替換
new_content = content.replace(old, new)
if new_content == content:
print("ERROR: Pattern not found, trying alternative method...")
# 使用更簡單的替換
new_content = content.replace(
'const response = await fetch("https://api.anthropic.com/v1/messages", {',
'const response = await fetch("/api/llm/generate", {'
)
new_content = new_content.replace(
'async function callClaudeAPI(prompt) {',
'async function callClaudeAPI(prompt, api = \'gemini\') {'
)
if new_content != content:
print("SUCCESS: Applied simple replacement")
else:
print("ERROR: Could not fix the file")
exit(1)
else:
print("SUCCESS: Pattern replaced")
# 寫回
with open('index.html', 'w', encoding='utf-8') as f:
f.write(new_content)
print("File updated: index.html")
print("\nNext steps:")
print("1. Start Flask backend: python app_updated.py")
print("2. Reload browser page (Ctrl+F5)")
print("3. Test AI generation")

255
scripts/rename_field_ids.py Normal file
View File

@@ -0,0 +1,255 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
欄位 ID 自動重命名腳本
根據 ID重命名對照表.md 批量替換 HTML 和 JavaScript 中的欄位 ID
"""
import re
import os
from pathlib import Path
# ID 重命名對照表
ID_MAPPINGS = {
# 模組 1: 崗位基礎資料 - 基礎資料頁籤 (15個)
'businessUnit': 'pos_businessUnit',
'division': 'pos_division',
'department': 'pos_department',
'section': 'pos_section',
'positionCode': 'pos_code',
'effectiveDate': 'pos_effectiveDate',
'positionName': 'pos_name',
'positionLevel': 'pos_level',
'positionCategory': 'pos_category',
'positionCategoryName': 'pos_categoryName',
'positionNature': 'pos_type',
'positionNatureName': 'pos_typeName',
'headcount': 'pos_headcount',
'positionDesc': 'pos_desc',
'positionRemark': 'pos_remark',
# 模組 2: 崗位基礎資料 - 招聘要求頁籤 (18個)
'minEducation': 'rec_eduLevel',
'requiredGender': 'rec_gender',
'salaryRange': 'rec_salaryRange',
'workExperience': 'rec_expYears',
'minAge': 'rec_minAge',
'maxAge': 'rec_maxAge',
'jobType': 'rec_jobType',
'recruitPosition': 'rec_position',
'jobTitle': 'rec_jobTitle',
'superiorPosition': 'rec_superiorCode',
'jobDesc': 'rec_jobDesc',
'positionReq': 'rec_positionReq',
'titleReq': 'rec_certReq',
'majorReq': 'rec_majorReq',
'skillReq': 'rec_skillReq',
'langReq': 'rec_langReq',
'otherReq': 'rec_otherReq',
'recruitRemark': 'rec_remark',
# 模組 3: 職務基礎資料 (12個)
'jobCategoryCode': 'job_category',
'jobCategoryName': 'job_categoryName',
'jobCode': 'job_code',
'jobName': 'job_name',
'jobNameEn': 'job_nameEn',
'jobEffectiveDate': 'job_effectiveDate',
'jobLevel': 'job_level',
'jobHeadcount': 'job_headcount',
'jobSortOrder': 'job_sortOrder',
'hasAttendanceBonus': 'job_hasAttBonus',
'hasHousingAllowance': 'job_hasHouseAllow',
'jobRemark': 'job_remark',
# 模組 4: 部門職責 (19個 - 包含合併重複欄位)
'deptFunctionCode': 'df_code',
'deptFunctionName': 'df_name',
'deptFunctionBU': 'df_businessUnit',
'deptFunc_businessUnit': 'df_businessUnit', # 合併
'deptFunc_division': 'df_division',
'deptFunc_department': 'df_department',
'deptFunc_section': 'df_section',
'deptFunc_positionTitle': 'df_posTitle',
'deptFunc_positionLevel': 'df_posLevel',
'deptManager': 'df_managerTitle',
'deptFunctionEffectiveDate': 'df_effectiveDate',
'deptHeadcount': 'df_headcountLimit',
'deptStatus': 'df_status',
'deptMission': 'df_mission',
'deptVision': 'df_vision',
'deptCoreFunctions': 'df_coreFunc',
'deptKPIs': 'df_kpis',
'deptCollaboration': 'df_collab',
'deptFunctionRemark': 'df_remark',
# 模組 5: 崗位描述 (8個需要變更的)
'jd_positionCode': 'jd_posCode',
'jd_positionName': 'jd_posName',
'jd_positionLevel': 'jd_posLevel',
'jd_positionEffectiveDate': 'jd_posEffDate',
'jd_directSupervisor': 'jd_supervisor',
'jd_positionGradeJob': 'jd_gradeJob',
'jd_workLocation': 'jd_location',
'jd_empAttribute': 'jd_empAttr',
'jd_deptFunctionCode': 'jd_dfCode',
'jd_positionPurpose': 'jd_purpose',
'jd_mainResponsibilities': 'jd_mainResp',
'jd_education': 'jd_eduLevel',
'jd_basicSkills': 'jd_basicSkills',
'jd_professionalKnowledge': 'jd_proKnowledge',
'jd_workExperienceReq': 'jd_expReq',
'jd_otherRequirements': 'jd_otherReq',
}
# 需要特殊處理的函數名映射onchange事件等
FUNCTION_MAPPINGS = {
'updateCategoryName': 'updateCategoryName', # 保持不變,但內部需要更新
'updateNatureName': 'updateTypeName', # positionNature -> pos_type
'updateJobCategoryName': 'updateJobCategoryName', # 保持不變
}
def replace_in_file(file_path, dry_run=False):
"""
在文件中替換所有匹配的 ID
Args:
file_path: 文件路徑
dry_run: 如果為 True只輸出變更不實際修改
Returns:
(總替換次數, 變更詳情列表)
"""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
original_content = content
changes = []
total_replacements = 0
# 排序按舊ID長度降序避免短ID誤替換長ID
sorted_mappings = sorted(ID_MAPPINGS.items(), key=lambda x: len(x[0]), reverse=True)
for old_id, new_id in sorted_mappings:
if old_id == new_id:
continue
# 匹配模式:
# 1. HTML id="oldId"
# 2. JavaScript getElementById('oldId') 或 getElementById("oldId")
# 3. HTML for="oldId"
# 4. HTML name="oldId"
# 5. 對象屬性 {oldId: ...} 或 data.oldId
patterns = [
# HTML id 屬性
(rf'\bid=["\']({re.escape(old_id)})["\']', rf'id="\1"', lambda m: f'id="{new_id}"'),
# HTML for 屬性
(rf'\bfor=["\']({re.escape(old_id)})["\']', rf'for="\1"', lambda m: f'for="{new_id}"'),
# HTML name 屬性
(rf'\bname=["\']({re.escape(old_id)})["\']', rf'name="\1"', lambda m: f'name="{new_id}"'),
# getElementById
(rf'getElementById\(["\']({re.escape(old_id)})["\']', rf'getElementById("\1")', lambda m: f'getElementById("{new_id}")'),
# 對象屬性訪問 .oldId (謹慎使用,確保前面是合理的對象)
(rf'\.({re.escape(old_id)})\b', rf'.\1', lambda m: f'.{new_id}'),
# 對象字面量屬性 oldId: 或 "oldId":
(rf'\b({re.escape(old_id)}):', rf'\1:', lambda m: f'{new_id}:'),
]
for pattern, _, replacement_func in patterns:
matches = list(re.finditer(pattern, content))
if matches:
# 從後往前替換,避免索引偏移
for match in reversed(matches):
start, end = match.span()
old_text = content[start:end]
new_text = replacement_func(match)
if old_text != new_text:
content = content[:start] + new_text + content[end:]
changes.append({
'old': old_text,
'new': new_text,
'line': content[:start].count('\n') + 1
})
total_replacements += 1
# 如果有變更且非 dry run寫回文件
if content != original_content and not dry_run:
with open(file_path, 'w', encoding='utf-8', newline='') as f:
f.write(content)
return total_replacements, changes
def main():
"""主函數"""
base_dir = Path(__file__).parent
# 需要處理的文件列表
files_to_process = [
base_dir / 'index.html',
base_dir / 'js' / 'ui.js',
base_dir / 'js' / 'ai-bags.js',
base_dir / 'js' / 'main.js',
]
print("=" * 80)
print("欄位 ID 重命名工具")
print("=" * 80)
print(f"\n📋 總計需要重命名:{len(ID_MAPPINGS)} 個 ID")
print(f"📂 需要處理:{len(files_to_process)} 個文件\n")
# 先 dry run 顯示變更
print("🔍 掃描變更Dry Run...")
print("-" * 80)
total_changes = 0
for file_path in files_to_process:
if not file_path.exists():
print(f"⚠️ 文件不存在:{file_path.name}")
continue
count, changes = replace_in_file(file_path, dry_run=True)
total_changes += count
if count > 0:
print(f"\n📄 {file_path.name}: {count} 處變更")
# 顯示前 5 個變更示例
for i, change in enumerate(changes[:5]):
print(f" L{change['line']}: {change['old']}{change['new']}")
if len(changes) > 5:
print(f" ... 還有 {len(changes) - 5} 處變更")
print("\n" + "=" * 80)
print(f"📊 總計:{total_changes} 處需要替換")
print("=" * 80)
# 詢問是否執行
response = input("\n是否執行替換?(y/n): ").strip().lower()
if response == 'y':
print("\n🚀 開始執行替換...")
print("-" * 80)
for file_path in files_to_process:
if not file_path.exists():
continue
count, _ = replace_in_file(file_path, dry_run=False)
if count > 0:
print(f"{file_path.name}: 已替換 {count}")
print("\n✨ 替換完成!")
print("\n⚠️ 請執行以下步驟:")
print(" 1. 測試所有表單功能")
print(" 2. 檢查瀏覽器控制台是否有錯誤")
print(" 3. 使用 git diff 檢查變更")
print(" 4. 提交變更git add -A && git commit -m 'refactor: 標準化欄位 ID 命名'")
else:
print("\n❌ 已取消執行")
if __name__ == '__main__':
main()