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:
281
scripts/add_csv_buttons.py
Normal file
281
scripts/add_csv_buttons.py
Normal 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!")
|
||||
487
scripts/add_dept_function.py
Normal file
487
scripts/add_dept_function.py
Normal 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")
|
||||
206
scripts/add_dept_relation.py
Normal file
206
scripts/add_dept_relation.py
Normal 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
84
scripts/add_org_fields.py
Normal 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!")
|
||||
499
scripts/add_position_list_and_admin.py
Normal file
499
scripts/add_position_list_and_admin.py
Normal 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!")
|
||||
258
scripts/add_random_positions.py
Normal file
258
scripts/add_random_positions.py
Normal 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
236
scripts/apply_cors_fix.py
Normal 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
26
scripts/check_models.py
Normal 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
99
scripts/complete_fix.py
Normal 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")
|
||||
93
scripts/convert_to_table.py
Normal file
93
scripts/convert_to_table.py
Normal 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")
|
||||
78
scripts/extract_dropdown_data.py
Normal file
78
scripts/extract_dropdown_data.py
Normal 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")
|
||||
123
scripts/extract_hierarchical_data.py
Normal file
123
scripts/extract_hierarchical_data.py
Normal 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
53
scripts/fix_csv_routes.py
Normal 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 路由重複問題")
|
||||
90
scripts/fix_gemini_model.py
Normal file
90
scripts/fix_gemini_model.py
Normal 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
382
scripts/generate_review.py
Normal 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)} 筆組織架構資料")
|
||||
|
||||
387
scripts/import_hierarchy_data.py
Normal file
387
scripts/import_hierarchy_data.py
Normal 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()
|
||||
252
scripts/improve_error_display.py
Normal file
252
scripts/improve_error_display.py
Normal 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
130
scripts/init_database.py
Normal 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
244
scripts/init_gitea.py
Normal 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
110
scripts/quick_fix.py
Normal 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
255
scripts/rename_field_ids.py
Normal 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()
|
||||
Reference in New Issue
Block a user